# Cycles

A **cycle** is a meaningful chunk of a project — a feature, a release, a phase — and it is the unit that actually executes. Tasks fan out inside a cycle run; they do not run on their own.

## Prerequisites

- A project with at least one cycle, usually planned by a [project run](/getting-started/quickstart/)
- Agency role (`owner`, `admin`, `member`); a client collaborator with steer rights can execute but not edit
- Model access configured for the workspace

## Proposing a cycle
**A proposal is not a cycle:** `POST /v1/api/cycles` returns **202** with `{"proposed":true,"adopted":false}`. It hands your title and description to the project orchestrator as a *proposal*. No cycle row comes back, and `adopted: false` means the orchestrator did not fold it into the roadmap on this pass. Poll `GET /v1/api/cycles?projectId=…` to see what was created.

```bash
curl -sS -X POST "$HLIX_BASE_URL/v1/api/cycles" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \
  -H 'content-type: application/json' \
  -d '{"projectId":"'"$PROJECT_ID"'","title":"Health and readiness","description":"Add liveness and readiness endpoints with tests and docs."}'
```

The response shape — `adopted` is `true` only when the orchestrator folded the proposal into the roadmap on this pass:

```json
{"proposed":true,"adopted":false}
```

`projectId`, `title` (1–200), and `description` (1–10 000) are required.

An adopted proposal becomes a cycle at status `triage`, and the [Triage Agent](/concepts/triage/) decides whether it goes any further. That is true of every intake path — chat, the API, Linear, or the project's founding brief — so a cycle in your list at `triage` has been created but not yet admitted.

## Executing a cycle

```bash
curl -sS -X POST "$HLIX_BASE_URL/v1/api/cycles/$CYCLE_ID/execute" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID"
```

Expected result:

```json
{"message":"Cycle execution started","threadId":"<workspace-id>:<cycle-id>"}
```

The optional `?mode=` query takes `sandbox` (default) or `desktop`.

Execution is gated on status: only `draft`, `planning`, `failed`, and `interviewing` cycles can be executed. A cycle still in `triage` is not executable — Triage admits it first. Re-executing a `failed` or `interviewing` cycle resets it to `draft` and returns every `failed` or `blocked` task to `queued` — a retry is a genuine retry, not a second parallel run. That reset is itself a recorded transition, attributed to you.

The response means the run was **queued**, not finished. If queuing itself fails you get a `502` and nothing was started:

```json
{"error":"The cycle run could not be queued, so it was not started. Nothing is running — try again."}
```

## Cycle statuses

| Status | Meaning |
| --- | --- |
| `triage` | Created and awaiting the [Triage Agent's](/concepts/triage/) decision |
| `draft` | Admitted, not yet running |
| `interviewing` | The orchestrator is clarifying requirements with you |
| `planning` | Decomposing the cycle into tasks |
| `executing` | Tasks are running |
| `e2e` | End-to-end verification |
| `pr_created` | Terminal — the work landed as a pull request |
| `failed` | Terminal — the cycle failed |
| `canceled` | Terminal — Triage refused the request |

## Stage history

Every status move is a **governed transition**: it is recorded on the cycle, attributed, and stamped with the platform rules in force at the time. `GET /v1/api/cycles/:id` returns the whole record as `stageHistory`, oldest first, alongside a `revision` counter.

```json
{
  "from": "triage",
  "to": "draft",
  "at": "2026-08-12T09:41:07.220Z",
  "actor": { "type": "agent", "id": "triage-agent" },
  "ruleSetVersion": "hlix-web-v1",
  "reason": "Schema change is understood and reversible."
}
```

| Field | What it answers |
| --- | --- |
| `from` / `to` | Which move this was |
| `at` | When |
| `actor.type` | Who decided: `human`, `agent`, or `rule` |
| `actor.id` | Which one — a user id, or a stable agent/rule identifier such as `triage-agent`, `orchestrator`, `gate-denied` |
| `ruleSetVersion` | Which version of the platform rules governed the move |
| `reason` | Free text, when the mover supplied one |

`actor.type` is the distinction that matters. `human` is a person acting through the dashboard or the API. `agent` is an LLM-driven decision — the Triage Agent forwarding a request, the orchestrator submitting a plan. `rule` is deterministic platform logic with no model involved, such as a denied approval failing the cycle.

`revision` is a compare-and-swap counter, incremented once per transition. Two writers racing the same cycle cannot both win: the loser is rejected with the row's real current revision rather than silently overwriting the winner.
**History records the move, not its legality:** Stage history is an attribution and concurrency record. It does not enforce which transitions are legal — that lives with the code performing each move, such as the planner's refusal to submit a plan for a cycle still in `triage`.

## Priority and lane

Triage writes two more fields onto every cycle it forwards.

- **`priority`** — an integer derived from the urgency tier Triage judged: `300` urgent, `200` high, `100` normal, `0` low. Ready cycles are dispatched highest first.
- **`lane`** — the risk lane the platform rules resolved from the complexity and risk judgement. Four lanes force a human approval before dispatch even when the project has not opted in.

Both are on the cycle row and in the published contract. [Triage](/concepts/triage/) covers how each is derived.

To list a project's cycles in dispatch order rather than creation order:

```bash
curl -sS "$HLIX_BASE_URL/v1/api/cycles?projectId=$PROJECT_ID&orderBy=priority" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID"
```

`orderBy` accepts `createdAt` (the default, newest first) or `priority` (highest first, ties keeping creation order). Any other value is a `400`. The dashboard's cycles grid offers the same two sorts.

## Watching a cycle

`GET /v1/api/cycles/:id/stream` streams the cycle's event timeline rather than a status field, so you see the run's narrative as it happens.

```bash
curl -N "$HLIX_BASE_URL/v1/api/cycles/$CYCLE_ID/stream" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID"
```

- Unnamed events carry one timeline event row each (`entityType`, `eventType`, `payload`, `actorType`, `traceId`, `timestamp`).
- `event: done` fires with `{"status":"pr_created"}` or `{"status":"failed"}` when the cycle reaches a terminal status, then the stream closes. Unlike the task stream, this one *does* announce its ending.
- `event: keepalive` with an empty payload arrives roughly every 30 seconds.
- The stream is bounded at 600 polls of 2 seconds — about 20 minutes. Reconnect for longer runs.

For a non-streaming read of the same material use `GET /v1/api/cycles/:id/timeline` (cycle plus child-task events) or `GET /v1/api/cycles/:id/events` (cycle events only).

## Reading a cycle

`GET /v1/api/cycles/:id` returns the cycle row plus its tasks — each with a `dependsOn` array — and `previewUrl` and `linearIssueUrl` when they exist.

```bash
curl -sS "$HLIX_BASE_URL/v1/api/cycles/$CYCLE_ID" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID"
```

`GET /v1/api/cycles?projectId=…` lists them.

## Managing the task set

| Operation | Endpoint | Constraint |
| --- | --- | --- |
| Add a task | `POST /v1/api/cycles/:id/tasks` | `title` and `description` required; `dependsOn` optional |
| Edit a task | `PATCH /v1/api/cycles/:id/tasks/:taskId` | `title`, `description`, `sortOrder` |
| Remove a task | `DELETE /v1/api/cycles/:id/tasks/:taskId` | Only `queued` or `blocked` tasks |
| Edit the cycle | `PATCH /v1/api/cycles/:id` | `title`, `description`, `targetBranch` |
| Delete the cycle | `DELETE /v1/api/cycles/:id` | Only `draft` or `failed` cycles |

Deleting a running cycle is refused rather than racing the engine:

```json
{"error":"Can only delete cycles in draft or failed status"}
```

## Talking to a cycle

`POST /v1/api/cycles/:id/messages` sends a message into the cycle thread (`content` required; `agentId` addresses one worker's 1:1 thread; `messageId` makes it idempotent). While the cycle is `interviewing`, a message with no `agentId` can advance the lifecycle — the response carries `lifecycleAdvanced` and `lifecycleReason`.

`POST /v1/api/cycles/:id/messages/stream` is the streaming form. Its SSE events are named, unlike the task stream:

| Event | Payload |
| --- | --- |
| `user_message` | `{"id":…,"content":…}` — your message, as stored |
| `token` | A raw text chunk, not JSON-wrapped |
| `tool_start` | `{"name":…,"input":…}` |
| `tool_end` | `{"name":…,"output":…}` |
| `complete` | `{"id":…,"content":…}` — the final assistant message |
| `error` | `{"message":…}` — then the stream returns |

## Artifacts and cost

- `GET /v1/api/cycles/:id/artifacts` lists what the run produced.
- `GET /v1/api/cycles/:id/artifacts/:filename` returns `{"url":…,"key":…}`, or the raw bytes with `?download=1`.
- `GET /v1/api/cycles/:id/usage` returns `{"totals":{…},"byRole":[…]}` with token counts and cost per agent role.

## If a cycle will not execute

- `{"error":"Cycle cannot be executed in \"executing\" status"}` — it is already running. Watch it instead of starting a second run.
- `{"error":"Cycle not found"}` — wrong id, or another workspace. Check `X-Organization-Id`.
- `{"error":"Project not found for cycle"}` — the cycle outlived its project; it cannot be run.
- `{"error":"The cycle run could not be queued, so it was not started. Nothing is running — try again."}` — a `502`. Nothing was half-started; retry is safe.
- `{"error":"Can only delete tasks in queued or blocked status"}` — the task is past the point where removing it is safe.
- `{"proposed":true,"adopted":false}` and no cycle appears — the orchestrator did not adopt the proposal. Re-read the roadmap before proposing again.
- `{"error":"Cycle cannot be executed in \"triage\" status"}` — the [Triage Agent](/concepts/triage/) has not admitted it yet. There is no way to execute past that decision.
- The cycle reads `canceled` — Triage refused the request. The reason is on the last `stageHistory` entry.
- The cycle parks and nothing happens — the project has approval required, **or** its lane demands one regardless. See [Approvals](/running/approvals/).

## Next steps

[Triage](/concepts/triage/)
  [Approvals](/running/approvals/)
  [Reviewing output](/running/review/)
  [Delivery metrics](/running/metrics/)