# How a Phase Reports Its Outcome > The TASK_RESULT block every phase ends with, what success, failure_reason and side_effects mean, how they become phase and execution status, and the one-artifact-per-phase rule Every phase of a workflow ends by saying two things about itself: **what it delivered** and **whether it succeeded**. The platform reads both, decides whether the phase completed, and records what the agent said beside what the platform observed. This page describes that contract from both sides: what an agent must write, and what you read back from the API. ## The TASK_RESULT block The prompt every phase is sent ends with a mandatory instruction: the last thing the agent writes must be a `TASK_RESULT` block. A block has three parts, the marker, one JSON object, and the terminator on the line after it: ```text TASK_RESULT: {"success": true, "side_effects": "none", "comments": "Opened PR #42 with the fix and a regression test"} TASK_RESULT_END ``` ```text TASK_RESULT: {"success": false, "failure_reason": "platform", "comments": "GH_TOKEN is not set, so the repository could not be cloned"} TASK_RESULT_END ``` | Key | Type | Meaning | | --- | --- | --- | | `success` | JSON boolean | Whether the phase produced what it was asked for. This is the only key that decides the outcome. | | `failure_reason` | one word | When `success` is `false`, what kind of failure it was. | | `side_effects` | one word | What happened to the external writes the phase attempted, such as a push or a PR comment. | | `comments` | string | A specific, human-readable explanation. Shown to operators. | ### `success` is the outcome, and it must be a boolean `success` must be the JSON boolean `true` or `false`. The block is parsed strictly: the string `"true"`, a number, or a word such as `"completed"` under `success` is not a boolean, so the block is **unreadable** (see below), and an unreadable block fails the phase. Two lenient readings exist, both logged as a warning so the drift stays visible: - A block whose JSON is wrapped in a markdown code fence (` ```json ... ``` `) between the marker and the terminator is read as if the fence were not there. - A block with **no** `success` key whose `status` is exactly the string `"completed"` or `"failed"` is read as success or failure. Any other `status` value, or a `status` key written twice, is unreadable. When `success` is a boolean, `status` is ignored; a `status` beside an unreadable `success` does not rescue it. ### `failure_reason`: what kind of failure When `success` is `false`, `failure_reason` is exactly one of four words. It is a label, never a sentence: the sentence goes in `comments`. | `failure_reason` | What the agent is saying | What someone does about it | | --- | --- | --- | | `task` | The request was wrong, impossible, or too big for one phase | Rewrite the brief | | `platform` | The machinery broke: a missing credential, a tool that crashed, a workspace that was not what it claimed | Fix the platform | | `refused` | Neither: the agent could have done the work and judged it should not | Read what it found | | `unknown` | The agent cannot honestly tell which of the three it was | Somebody reads the run | `failure_reason` never changes **whether** a phase completes; `success` alone decides that. A word the platform does not recognise, a sentence, or a missing key is recorded as "no reason given", logged, and leaves the outcome unchanged. ### `side_effects`: what happened to external writes `side_effects` is exactly one of four words: | `side_effects` | When | | --- | --- | | `none` | The phase attempted no external write | | `succeeded` | It made external writes and every one went through | | `denied` | A write was refused: permissions, a protected branch, a read-only token | | `failed` | A write was attempted and broke: network, API error, a tool that crashed | **`success` is about the deliverable, not every action around it.** A phase that produced its deliverable and was then refused a PR comment reports `success: true` and `side_effects: denied`. The phase completes, the deliverable is kept, and the refusal is recorded beside it for an operator to act on. Like `failure_reason`, `side_effects` never decides whether a phase completes, and an unrecognised word is recorded as "not reported". ### What counts as a report A report is delimited, not searched for. Only text that starts with `TASK_RESULT:`, holds one JSON value, and is closed by `TASK_RESULT_END` is a report. The agent's verdict is read as each message arrives, so a later sign-off message cannot overwrite an earlier report. The platform reads each phase into one of four states: | State | What happened | Does it fail the phase? | | --- | --- | --- | | Success | A readable block with `success: true` | No | | Failure | A readable block with `success: false` | Yes | | Unreadable | The marker was written but no closed, readable block followed it (not JSON, wrong type, or no `TASK_RESULT_END`) | Yes | | Not reported | No `TASK_RESULT:` marker at all | No, on its own | If an agent writes more than one report, the strongest claim stands, regardless of order: failure beats success, success beats unreadable, unreadable beats not reported. A failure that the agent reported cannot be taken back by a later success. **Not reported is not a failure by itself.** A phase that never writes the marker is governed by the other checks that apply to every phase: its exit status and the artifact rule below. The prompt still requires the block, and a phase that omits it gives operators nothing to read about its outcome. ## From the report to phase and execution status The phase's report is weighed together with what the platform observed about the run itself: the process exit code, whether the output stream was intact, and whether the run was cancelled. | The run | The report | Phase | Execution `failure_classification` | | --- | --- | --- | --- | | Exit 0, stream intact, not cancelled | Success or not reported | `completed` (if the artifact rule below is met) | none | | Exit 0, stream intact, not cancelled | Failure | `failed` | `correct_refusal`, or `unclassified` if `failure_reason` was `unknown` | | Exit 0, stream intact, not cancelled | Unreadable | `failed` | `platform` | | Exit 0, stream broken | Success or not reported | `completed` | none | | Non-zero exit, timeout, crash, or broken stream with a failure report | Any | `failed` | `platform` | | Cancelled by an operator | Any | not completed; its output is kept as partial, and the execution is `cancelled` | none | When a phase fails, the execution fails with it, and `status` on the execution is `failed`. Two fields on the execution keep the platform's measurement and the agent's words apart: - **`failure_classification`** is what the **platform** concluded, and failure rates are computed from it. Only a cleanly finished run with a readable `success: false` is a `correct_refusal`: the system working as designed. Everything else that fails is `platform`. The agent's `failure_reason` can never move a failure into `task` or out of the platform's count. The one thing it can do is withdraw the claim: `unknown` records the failure as `unclassified`. - **`reported_failure_reason`** is what the **agent said**, recorded verbatim (`task`, `platform`, `refused`, `unknown`, or `null` when it named nothing recognised). Read it as a quotation. ## Reading the outcome from the API `GET /api/v1/executions/{execution_id}` returns, among its other fields: | Field | Meaning | | --- | --- | | `status` | `running`, `completed`, `failed`, `cancelled`, `interrupted` | | `failure_classification` | What the platform concluded about a failure: `platform`, `correct_refusal`, `unclassified` | | `reported_failure_reason` | What the failing phase's agent wrote as `failure_reason`, or `null` | | `deliverable_produced` | `true` when an artifact is linked to the execution or to one of its phases, **whatever `status` says** (see the limitation below) | | `reported_side_effects` | The most severe `side_effects` any phase reported, or `null` if none did | | `phases[].deliverable_recovered` | `true` when this phase's deliverable was recovered from its last message (see below) | | `phases[].reported_side_effects` | What this phase's agent reported, or `null` | `deliverable_produced` and `status` are independent on purpose. A run can fail after its deliverable exists, and a run can complete while a write-back was refused. To decide what to do next: | `status` | `deliverable_produced` | `reported_side_effects` | Read it as | | --- | --- | --- | --- | | `completed` | `true` | `none` or `succeeded` | Done | | `completed` | `true` | `denied` | The work is finished; a write was refused. Grant the permission, do not re-run the work | | `completed` | `true` | `failed` | The work is finished; a write broke. Retry the write | | `failed` | `true` | any | The run failed, but there is work to read. Open the artifacts before re-running | | `failed` | `false` | any | Nothing was kept | `reported_side_effects` ranks `failed` over `denied` over `succeeded` over `none`, so one refused write in any phase is not hidden by another phase's success. It is collected from phases that **completed**: a failed phase's `side_effects` word is not recorded. It is a report, never a measurement: nothing verifies what the agent says happened. Both execution-level fields cover only the phases this execution ran. A [resumed execution](/docs/guide/resuming-executions)'s inherited phases are recorded on its parent. `deliverable_produced` reads only the artifacts the execution detail links: those from completed phases and those kept by a phase that **failed**. The partial artifact kept by a phase that was **cancelled** or **interrupted** is stored, but nothing links it to the execution detail yet, so it does not count. A cancelled or interrupted run whose only artifact is that partial one reads `deliverable_produced: false` even though the artifact exists. Look for it in the execution's artifacts before concluding nothing was kept. ```bash curl -s -u "$SYN_API_USER:$SYN_API_PASSWORD" \ "$SYN_API_URL/api/v1/executions/$EXEC" | jq '{status, failure_classification, reported_failure_reason, deliverable_produced, reported_side_effects, phases: [.phases[] | {name, status, deliverable_recovered, reported_side_effects}]}' ``` ## Every phase produces an artifact Inside a workspace, `/workspace/artifacts/output/` is the **only** directory collected when a phase ends. Every file under it becomes an artifact of the phase (files inside `__pycache__` and `.pytest_cache` directories are skipped as build junk), and the next phase receives them in `artifacts/input/`. The rule is that every phase produces at least one artifact, whatever its outcome. The platform enforces it in three steps. ### 1. Files the phase wrote Each collectable file under `artifacts/output/` is judged on its own. A non-empty file is stored as-is. ### 2. The last message, recovered A phase that wrote **no collectable file** has its agent's **last message** salvaged instead. So does every file it wrote that was **empty**, even when another file beside it has content: the non-empty files are stored as-is and each empty one is replaced by the recovered last message, filed under that file's own path. The recovered artifact is clearly marked so nobody mistakes it for a document the phase wrote: - its title ends in `[recovered from transcript]`; - its content starts with a banner saying it was recovered from the session transcript and is the agent's last message, not its intended deliverable; - when the phase left work on a branch, a "Where this phase's work stands" section names it; - with no file at all, it is filed as `artifacts/output/recovered-from-transcript.md`. The phase **completes**, and `phases[].deliverable_recovered` is `true`, which is the only field that tells a recovered deliverable apart from a written one. ### 3. Nothing usable: the phase fails Recovery only accepts a last message that reports something a downstream phase could act on. A message needs at least **eight words of substance** after removing segments that only announce completion ("Done.", "All set", "Task complete", "Thanks") and segments that state a refusal without giving a reason ("I cannot do that."). A refusal **with** its reason ("I could not push the branch because the credential rejects workflow changes") counts. So a phase that writes no file and ends on a bare "Done." **fails**: there is nothing on disk and nothing said that the next phase could build on. The same is true of a phase that wrote one file with content and one empty file and then ended on "Done.": the empty file cannot be recovered, and the phase fails rather than skipping it, even though the other file had content. The error names the phase, and the output types it declared if it declared any. Recovery is a safety net, not a substitute. Have every phase prompt write its deliverable to `artifacts/output/`. A recovered artifact is the agent's closing message, which may be a full conclusion or only a sign-off. ### Failed and interrupted phases keep their work A phase that will not complete still keeps what it produced, before its workspace is torn down: | How the phase ended | What is kept | Artifact title marker | | --- | --- | --- | | It reported `success: false`, wrote an unreadable report, or exited non-zero | Every non-empty file under `artifacts/output/`; if there were none, its last message (same eight-word rule) | `(kept from a failed phase)` | | It was cancelled or interrupted | Every non-empty file under `artifacts/output/`; if there were none, its last message (same rule) | `(partial)` | Keeping the work does not change the phase's outcome: it stays `failed`, `cancelled` or `interrupted` respectively. One exception: when the platform retries an attempt whose terminal event was lost, that attempt keeps nothing, because the phase is not over and the attempt that finishes it produces its artifact. Artifacts kept by a **failed** phase count towards `deliverable_produced`, so a failed run whose work survived reads `failed` with `deliverable_produced: true`. Artifacts kept by a cancelled or interrupted phase do not count yet; see the limitation under [Reading the outcome from the API](#reading-the-outcome-from-the-api). ### Workflow changes the platform could not push When a failed phase left unpushed commits that change `.github/workflows/`, the platform quarantines them in a way GitHub will accept and stores the dropped workflow changes as a patch **artifact** of that phase. How the patch is produced and how to apply it is described in [Work That Changed a GitHub Actions Workflow](/docs/workspaces/hydration#work-that-changed-a-github-actions-workflow). ## Learn More - [Workflows](/docs/guide/workflows): defining phases and their prompts - [Resuming Failed Executions](/docs/guide/resuming-executions): continue a failed run from the phase that did not finish - [Observability](/docs/guide/observability): watching a run as it happens - [Repository Hydration](/docs/workspaces/hydration): how unpushed work is saved when a phase fails