> ## Documentation Index
> Fetch the complete documentation index at: https://docs.substrate.42.pe/llms.txt
> Use this file to discover all available pages before exploring further.

# Gates Walkthrough: Complete Agent Session with Real Payloads

> Follow a real eight-operation session showing task creation, blocked transitions, gate satisfaction, human-only approval, revocation, and event history.

This page is a transcript of an actual session. Every response is real; long state echoes are abridged for readability. Follow along to see how transition guards, human-only fields, version discipline, and the event log behave together.

## The board

Four stages, six custom fields, four policies:

```text theme={null}
Spec  ──gate-build──▶  Build  ──gate-review──▶  Review  ──┐
                                                          ├──gate-done──▶  Done
                          (any group) ─────────────────────┘
```

**Groups:** `spec` (position 0), `build` (position 1), `review` (position 2), `done` (position 3)

**Fields:**

```json theme={null}
"field_schema": {
  "task": {
    "priority":         { "type": "enum", "values": ["low", "med", "high"] },
    "spec_doc":         { "type": "string" },
    "plan_approved":    { "type": "boolean" },
    "tests_passing":    { "type": "boolean" },
    "reviewer":         { "type": "string" },
    "shipped_approved": { "type": "boolean", "human_only": true }
  }
}
```

**Policies:**

| Id            | Type                   | Rule                                                                    |
| ------------- | ---------------------- | ----------------------------------------------------------------------- |
| `gate-build`  | `transition_guard`     | `spec → build`, requires `plan_approved = true`                         |
| `gate-review` | `transition_guard`     | `build → review`, requires `tests_passing = true`                       |
| `gate-done`   | `transition_guard`     | `* → done`, requires `reviewer` not empty AND `shipped_approved = true` |
| `resp-tests`  | `agent_responsibility` | when `group_id = build`, message: "Write tests as you go…"              |

Validate the board before the session starts:

```sh theme={null}
$ substrate validate
Board delivery (Delivery): 4 policies, 4 groups
✓ 1 board(s), 4 policies, 0 member(s) — loaded; definitions parse + group refs resolve, no warnings.
```

## Operation 1 — Create work

Create a task in Spec and another directly in Build:

<CodeGroup>
  ```json Create in Spec — request theme={null}
  {
    "board_id": "delivery",
    "group_id": "spec",
    "title": "Add CSV export to the reports page",
    "description": "Users want the report table as a file.",
    "custom_data": { "priority": "high", "spec_doc": "docs/specs/csv-export.md" },
    "agent_name": "claude-code"
  }
  ```

  ```json Create in Spec — response theme={null}
  {
    "ok": true,
    "applied": {
      "entity": "task",
      "id": "6fb97c36-…",
      "version": 1,
      "state": { "…": "…" }
    },
    "policies_fired": []
  }
  ```
</CodeGroup>

Now create one directly in Build:

<CodeGroup>
  ```json Create in Build — request theme={null}
  {
    "board_id": "delivery",
    "group_id": "build",
    "title": "Fix flaky signup test",
    "custom_data": { "priority": "low", "plan_approved": true },
    "agent_name": "claude-code"
  }
  ```

  ```json Create in Build — response theme={null}
  {
    "ok": true,
    "applied": { "entity": "task", "id": "68be5428-…", "version": 1, "state": { "…": "…" } },
    "policies_fired": [
      {
        "policy_id": "resp-tests",
        "policy_name": "Tests during development",
        "policy_type": "agent_responsibility",
        "message": "Write tests as you go — tests_passing gates the move to Review."
      }
    ]
  }
  ```
</CodeGroup>

<Note>
  This task entered Build without passing `gate-build`. Guards fire on **transitions**, not on creation. A task can be created directly in any group.
</Note>

## Operation 2 — Move without meeting the gate

Check first, then attempt the move:

<CodeGroup>
  ```json check_transition — request theme={null}
  { "task_id": "6fb97c36-…", "to_group": "build" }
  ```

  ```json check_transition — response theme={null}
  {
    "allowed": false,
    "from_group": "spec",
    "to_group": "build",
    "blocked_by": {
      "policy_id": "gate-build",
      "message": "Set plan_approved=true once the plan is signed off."
    }
  }
  ```

  ```json update_task attempt — request theme={null}
  { "id": "6fb97c36-…", "version": 1, "group_id": "build", "agent_name": "claude-code" }
  ```

  ```json update_task attempt — response theme={null}
  {
    "ok": false,
    "error": {
      "code": "transition_blocked",
      "message": "Set plan_approved=true once the plan is signed off.",
      "details": { "policy_id": "gate-build", "from_group": "spec", "to_group": "build" }
    }
  }
  ```
</CodeGroup>

Three things to note here:

* The `on_failure_message` from the policy *is* the error message — write it as an instruction to the agent.
* A blocked write changes nothing, including the task's version.
* The block is recorded as a `move_blocked` event in the task's history.

## Operation 3 — Satisfy the gate and move in one write

The gate evaluates the task as it would look **after** the move. Set the required field and change `group_id` in a single `update_task` call:

<CodeGroup>
  ```json update_task — request theme={null}
  {
    "id": "6fb97c36-…",
    "version": 1,
    "group_id": "build",
    "custom_data": { "plan_approved": true },
    "agent_name": "claude-code"
  }
  ```

  ```json update_task — response theme={null}
  {
    "ok": true,
    "applied": { "entity": "task", "id": "6fb97c36-…", "version": 2, "state": { "…": "…" } },
    "policies_fired": [
      {
        "policy_id": "resp-tests",
        "policy_type": "agent_responsibility",
        "message": "Write tests as you go — tests_passing gates the move to Review."
      }
    ]
  }
  ```
</CodeGroup>

<Tip>
  `custom_data` is **merged**, not replaced. The call only sent `plan_approved`, but `priority` and `spec_doc` remain on the task unchanged.
</Tip>

## Operation 4 — Stale version

If another write has occurred since your last read, you will receive `version_mismatch`:

<CodeGroup>
  ```json Stale write — request theme={null}
  { "id": "6fb97c36-…", "version": 1, "title": "Add CSV export", "agent_name": "claude-code" }
  ```

  ```json Stale write — response theme={null}
  {
    "ok": false,
    "error": {
      "code": "version_mismatch",
      "message": "This task has been updated since you last read it. Call get_task to re-read, reconcile any conflicts, then retry with the new version.",
      "details": { "id": "6fb97c36-…", "current_version": 2 }
    }
  }
  ```
</CodeGroup>

<Warning>
  The correct response is to re-read with `get_task`, reconcile any conflicts, then retry. Do **not** resend the same body with `version: 2` — that would silently overwrite another writer's changes.
</Warning>

## Operation 5 — Second gate

Moving from Build to Review requires `tests_passing = true` (`gate-review`). Attempting the move without it fails with the same pattern as Operation 2. Set `tests_passing: true` and move in one call — the task lands in Review at `version: 3`.

<Warning>
  Nothing ran the tests. The agent set `tests_passing: true` itself. This gate is a confession step, not a verification.
</Warning>

## Operation 6 — The human gate

`gate-done` requires both `reviewer` (not empty) and `shipped_approved = true`. The `shipped_approved` field is `human_only`.

<CodeGroup>
  ```json check_transition — request theme={null}
  { "task_id": "6fb97c36-…", "to_group": "done" }
  ```

  ```json check_transition — response theme={null}
  {
    "allowed": false,
    "from_group": "review",
    "to_group": "done",
    "blocked_by": {
      "policy_id": "gate-done",
      "message": "Done needs a reviewer and a human sign-off: substrate approve <task_id> shipped_approved"
    }
  }
  ```
</CodeGroup>

The agent attempts to set both fields at once:

<CodeGroup>
  ```json Agent sets both fields — request theme={null}
  {
    "id": "6fb97c36-…",
    "version": 3,
    "custom_data": { "reviewer": "dana", "shipped_approved": true },
    "agent_name": "claude-code"
  }
  ```

  ```json Agent sets both fields — response theme={null}
  {
    "ok": false,
    "error": {
      "code": "forbidden",
      "message": "Field(s) shipped_approved are human-only — a human must set them (e.g. `substrate approve 6fb97c36-54cb-466b-a010-ab5fb4097461 shipped_approved`). An agent cannot set them via update_task.",
      "details": { "fields": ["shipped_approved"] }
    }
  }
  ```
</CodeGroup>

<Note>
  The error code is `forbidden`, not `transition_blocked`. The **entire write** is rejected — including the legitimate `reviewer` field — before anything is applied. Split the call: set only `reviewer` first, then wait for human approval.
</Note>

After setting `reviewer` separately, the agent calls `list_pending_approvals` to surface what needs human attention:

```json theme={null}
{
  "project_name": "demo",
  "count": 3,
  "items": [
    {
      "board_id": "delivery",
      "board_name": "Delivery",
      "task_id": "6fb97c36-…",
      "task_title": "Add CSV export to the reports page",
      "group_id": "review",
      "gate": { "policy_id": "gate-done", "to_group": "done" },
      "awaiting_fields": ["shipped_approved"]
    }
  ]
}
```

<Warning>
  The count is 3, not 1, because `gate-done` is a `"*" → done` wildcard — every task on the board is one move away from a gate it cannot pass. A wildcard human gate makes this report noisy on a busy board. If that bothers you, gate a specific `from_group` instead.
</Warning>

## Operation 7 — The human decides

```sh theme={null}
$ substrate pending-approval
3 task(s) pending human approval in demo:

Delivery
  Task                                  Move           Awaiting          Approve
  Add CSV export to the r…  (6fb97c36)  review → done  shipped_approved  substrate approve 6fb97c36-54cb-466b-a010-ab5fb4097461 shipped_approved
  Fix flaky signup test     (68be5428)  build → done   shipped_approved  substrate approve 68be5428-0a59-4f30-b2a2-0c4ee6c1b652 shipped_approved
  Rate-limit the public A…  (3c2559b6)  spec → done    shipped_approved  substrate approve 3c2559b6-5c54-4b8e-9a39-3d0ceb6f2a4f shipped_approved
```

Each row carries the exact command that clears it. Run one:

```sh theme={null}
$ substrate approve 6fb97c36-54cb-466b-a010-ab5fb4097461 shipped_approved
Approved: set shipped_approved = true on task 6fb97c36-54cb-466b-a010-ab5fb4097461 (as human:diegoferreyra, version 5).
```

<Note>
  The approval is stamped `human:diegoferreyra`, not the agent name. It bumps the version to 5 — the agent's cached version 4 is now stale and must be re-read before the next write.
</Note>

The agent re-checks and moves:

```json theme={null}
{ "allowed": true, "from_group": "review", "to_group": "done" }
```

```json theme={null}
{
  "ok": true,
  "applied": { "version": 6, "state": { "group_id": "done" } },
  "policies_fired": []
}
```

<Warning>
  `substrate approve` sets the field; it does **not** move the task. The human decides; the agent performs the move.
</Warning>

## Operation 8 — Revoking an approval

```sh theme={null}
$ substrate unapprove 6fb97c36-54cb-466b-a010-ab5fb4097461 shipped_approved
Revoked: cleared shipped_approved (was true) on task … (as human:diegoferreyra, version 7).
Warning: task … is in group 'done', past the gate that required shipped_approved.
         unapprove does not move tasks — move it back on the board if that was a mistake.
```

<Note>
  The field is **deleted**, not set to `false`. Both an `exists` check and an `eq true` check will re-block correctly if the task is moved back through the gate.
</Note>

## What the history remembers

| #  | Event          | What it records                                             |
| -- | -------------- | ----------------------------------------------------------- |
| 1  | `created`      | Initial state — group `spec`, title, `custom_data`          |
| 4  | `move_blocked` | `gate-build`, `spec → build`, failure message               |
| 5  | `updated`      | Before/after group and `custom_data`, plus `policies_fired` |
| 6  | `move_blocked` | `gate-review`, `build → review`                             |
| 7  | `updated`      | Into Review with `tests_passing`                            |
| 8  | `updated`      | `reviewer` set. Actor `claude-code`                         |
| 9  | `move_blocked` | `gate-done`, `review → done`                                |
| 10 | `updated`      | `shipped_approved` set. Actor `human:diegoferreyra`         |
| 11 | `updated`      | `review → done`. Actor `claude-code`                        |

Event 10, verbatim:

```json theme={null}
{
  "id": 10,
  "task_id": "6fb97c36-…",
  "event_type": "updated",
  "changes": {
    "before": { "custom_data": { "…": "…", "reviewer": "dana" } },
    "after":  { "custom_data": { "…": "…", "reviewer": "dana", "shipped_approved": true } }
  },
  "actor_agent_name": "human:diegoferreyra",
  "occurred_at": "2026-09-06T23:32:45.074Z"
}
```

The blocked attempts are as informative as the successful ones. Three `move_blocked` events show the rails engaged three times, and the single `human:` actor shows exactly where a person entered the loop.

## What this board does and doesn't give you

<CardGroup cols={2}>
  <Card title="What you get" icon="check-circle">
    An agent that cannot quietly skip a stage. Reminders delivered at the moment they apply. A durable record of every attempt including refused ones. One step that genuinely waits on a person.
  </Card>

  <Card title="What you don't get" icon="x-circle">
    Verified tests — self-attested. Immutable rails — policies are data, and an agent could `archive_policy`, but that act is git-visible and event-logged.
  </Card>
</CardGroup>

<CardGroup cols={2}>
  <Card title="Policies and Gates" icon="shield" href="/concepts/policies-and-gates">
    Full reference for `transition_guard` and `agent_responsibility` — operators, compounds, and what guards guarantee.
  </Card>

  <Card title="Validate Your Gates" icon="flask" href="/authoring/validate-gates">
    Run `substrate validate` and test your policies before relying on them in a live workflow.
  </Card>
</CardGroup>
