# A Long-Running Agent with Sessions

Build an agent that watches each new account for fifteen minutes: start a session, resume it with entries, approve a tool call, answer a held turn, close a session, and see one end.

This tutorial builds an agent with a **session**: one durable conversation per account that lives across many events. Opening an account starts a session for it, every entry posted to the account resumes that session, and fifteen minutes after the account opened the session takes a final turn and ends.

On the way you list sessions and their turns, read what each turn received and did, approve a tool call the agent paused before, answer a turn the agent declined to continue, and close a session by hand.

> **Note:**
>
> You need a running [local environment](https://www.twisp.com/docs/infrastructure/local-environment.md). Open GraphiQL in the local console and run each step as its own request.

The agent uses the `fixture` model, which answers from a script instead of calling a model, so every run behaves the same and costs nothing. The steps take about 20 minutes, most of it waiting for the session to end. If you have run this tutorial before, choose fresh UUIDs, action names, account codes, and a fresh tran code.

## 1. Create the tool

The agent will have one tool, `notify_ops`, which sends a note about an account to an operations team. This version only returns what it was given, so you can see each call's input and output.

```graphql
mutation CreateNotifyOps {
  actions {
    createAction(
      id: "8156f490-be78-47a7-830a-328960b15ead"
      input: {
        name: "sessions-notify-ops"
        description: "Send a note about an account to the operations team."
        kind: "tool"
        config: {
          runtime: "starlark"
          src: """
          def run(event):
              return {"status": "sent", "account_id": event["data"]["account_id"], "note": event["data"]["note"]}
          """
        }
        tool: {
          name: "notify_ops"
          description: "Send a note about an account to the operations team."
          inputSchema: {
            type: "object"
            properties: { account_id: { type: "string" }, note: { type: "string" } }
            required: ["account_id", "note"]
            additionalProperties: false
          }
        }
      }
    ) {
      actionId
      status
      pending
    }
  }
}
```

The response reports `status: UNPUBLISHED` and `pending: ACTIVE`. The agent can reference the tool before it publishes.

## 2. Create the agent

```graphql
mutation CreateAccountWatch {
  actions {
    createAction(
      id: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      input: {
        name: "sessions-account-watch"
        description: "Watch each new account for its first fifteen minutes."
        kind: "agent"
        agent: {
          prompt: "Watch this new account. Review each turn's inputs and tell operations about anything unusual."
        }
        config: {
          runtime: "strands"
          harness: { builtin_tools: [] }
          model: {
            provider: "fixture"
            script: [
              {
                when: "purpose == 'turn' && session.final"
                responses: [[{ text: "Final review: nothing further to report." }]]
              }
              {
                when: "purpose == 'turn' && session.step == 0"
                responses: [
                  [
                    {
                      toolUse: {
                        name: "notify_ops"
                        input: { note: "First deposit received. Please confirm identity checks." }
                      }
                    }
                  ]
                ]
              }
              {
                when: "purpose == 'turn' && session.step == 2"
                responses: [
                  {
                    refusal: {
                      category: "review"
                      explanation: "A person should look at this deposit first."
                    }
                  }
                ]
              }
              {
                when: "purpose == 'turn'"
                responses: [[{ text: "Reviewed the new inputs. Nothing unusual." }]]
              }
              {
                when: "purpose == 'outcome' && session.final"
                responses: [
                  {
                    outcome: {
                      reply: "Watch period over."
                      session: {
                        status: "COMPLETED"
                        reason: "Watch period ended with no findings."
                      }
                    }
                  }
                ]
              }
              {
                when: "purpose == 'outcome'"
                responses: [{ outcome: { reply: "Still watching.", session: { status: "OPEN" } } }]
              }
            ]
          }
        }
        tools: [
          { name: "sessions-notify-ops", input: { account_id: "session.id" }, approval: REQUIRED }
        ]
        triggers: [
          {
            name: "opened"
            event: "event.type == 'account.inserted' && event.data.metadata.?watch.orValue(false) == true"
            session: { start: { input: { account_id: "string(event.data.account_id)" } } }
          }
          {
            name: "entry"
            event: "event.type == 'entry.inserted'"
            session: { resume: { id: "string(event.data.account_id)" } }
          }
        ]
        session: {
          inputSchema: {
            type: "object"
            properties: { account_id: { type: "string" } }
            required: ["account_id"]
            additionalProperties: false
          }
          id: "session.input.account_id"
          ends: "session.started_at + duration('15m')"
          schedule: [{ name: "tick", every: "1m" }]
          turns: { on: ["entry"] }
        }
      }
    ) {
      actionId
      status
      pending
    }
  }
}
```

How the definition fits together:

| Part               | What it does                                                                                                                                                                                                                                                                 |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `opened` trigger   | A **start** trigger. An account inserted with `metadata.watch` set to `true` starts a session, and its `input` maps the event to the session's input, `{account_id}`. Accounts without the flag start nothing.                                                               |
| `entry` trigger    | A **resume** trigger. Each entry is delivered to the open session whose ID is the entry's `account_id`. An entry on an account with no session is not delivered anywhere.                                                                                                    |
| `session.id`       | Each session's ID is its account ID, so the same account always addresses the same session.                                                                                                                                                                                  |
| `session.ends`     | Each session ends 15 minutes after it started. Before then the agent can only keep it open; at the end it gets a final turn that must complete it.                                                                                                                           |
| `session.schedule` | The session's own clock: `tick` fires every minute after the session starts, as an input to that session alone.                                                                                                                                                              |
| `turns.on`         | Only `entry` inputs start a turn. The account event and `tick` firings wait and ride along with the next turn.                                                                                                                                                               |
| `tools`            | The agent may call `notify_ops`. `input` fixes `account_id` to the session's ID, so the model supplies only `note`, and `approval: REQUIRED` makes every call wait for a person.                                                                                             |
| `model.script`     | The fixture's answers, chosen by `when`. `session.step` counts the session's earlier turns, so the first turn (`step == 0`) calls `notify_ops` and the third (`step == 2`) declines to continue. Other turns reply and keep the session `OPEN`; the final turn completes it. |
| `harness`          | `builtin_tools: []` turns off the agent tools, which this agent does not need.                                                                                                                                                                                               |

Poll until the agent is live:

```graphql
query ReadAccountWatch {
  actions {
    action(id: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0") {
      status
      pending
      definition {
        session {
          id
          ends
          schedule {
            name
            every
          }
          turns {
            on
          }
        }
      }
    }
  }
}
```

```json
{
  "status": "ACTIVE",
  "pending": null,
  "definition": {
    "session": {
      "id": "session.input.account_id",
      "ends": "session.started_at + duration('15m')",
      "schedule": [
        {
          "name": "tick",
          "every": "1m0s"
        }
      ],
      "turns": {
        "on": ["entry"]
      }
    }
  }
}
```

> **Note:**
>
> Wait for `status: ACTIVE` with `pending: null` before opening accounts. Actions respond to new events only.

## 3. Set up deposits

To post entries you need a settlement account to move funds from and a tran code. The settlement account has no `watch` flag, so it starts no session.

```graphql
mutation SetUpDeposits {
  settlement: createAccount(
    input: {
      accountId: "c74264c6-7659-4b40-b0e0-1559b0d34aa0"
      code: "SESSIONS.SETTLEMENT"
      name: "Sessions tutorial settlement"
      normalBalanceType: DEBIT
    }
  ) {
    accountId
  }
  deposit: createTranCode(
    input: {
      tranCodeId: "89c65b8c-659d-41ea-b9b0-70323bd411fd"
      code: "SESSIONS_DEPOSIT"
      description: "Move funds from settlement into a customer account."
      params: [{ name: "account", type: UUID }, { name: "amount", type: DECIMAL }]
      transaction: { description: "'Deposit'" }
      entries: [
        {
          accountId: "uuid('c74264c6-7659-4b40-b0e0-1559b0d34aa0')"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'DEPOSIT_DR'"
          direction: "DEBIT"
          layer: "SETTLED"
        }
        {
          accountId: "params.account"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'DEPOSIT_CR'"
          direction: "CREDIT"
          layer: "SETTLED"
        }
      ]
    }
  ) {
    tranCodeId
  }
}
```

## 4. Open two watched accounts

Open Alice's and Bob's accounts with `metadata: { watch: true }`. Each insert matches the `opened` trigger and starts a session.

```graphql
mutation OpenWatchedAccounts {
  alice: createAccount(
    input: {
      accountId: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
      code: "SESSIONS.ALICE"
      name: "Alice"
      normalBalanceType: CREDIT
      metadata: { watch: true }
    }
  ) {
    accountId
  }
  bob: createAccount(
    input: {
      accountId: "5a1933de-63f9-44ff-83aa-24987e2d9c39"
      code: "SESSIONS.BOB"
      name: "Bob"
      normalBalanceType: CREDIT
      metadata: { watch: true }
    }
  ) {
    accountId
  }
}
```

List the agent's sessions:

```graphql
query ListSessions {
  actions {
    action(id: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0") {
      sessions(first: 10) {
        nodes {
          sessionId
          status
          input
          startedAt
          endsAt
          turns(first: 1) {
            nodes {
              number
            }
          }
        }
      }
    }
  }
}
```

```json
{
  "nodes": [
    {
      "sessionId": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b",
      "status": "OPEN",
      "input": {
        "account_id": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
      },
      "startedAt": "2026-10-11T18:03:35.390689Z",
      "endsAt": "2026-10-11T18:18:35.390689Z",
      "turns": {
        "nodes": []
      }
    },
    {
      "sessionId": "5a1933de-63f9-44ff-83aa-24987e2d9c39",
      "status": "OPEN",
      "input": {
        "account_id": "5a1933de-63f9-44ff-83aa-24987e2d9c39"
      },
      "startedAt": "2026-10-11T18:03:35.390689Z",
      "endsAt": "2026-10-11T18:18:35.390689Z",
      "turns": {
        "nodes": []
      }
    }
  ]
}
```

There is one session per account, each `OPEN`, with an `endsAt` fifteen minutes after `startedAt`. Neither has a turn yet: the account event is not one of the inputs `turns.on` names, so it waits in the session for the first entry.

## 5. Post a deposit and read the turn

Deposit into Alice's account. The deposit's entry on her account resumes her session and starts turn 1.

```graphql
mutation FirstDeposit {
  postTransaction(
    input: {
      transactionId: "fb49b488-1d87-49a4-95f0-cf1cbcdc5b98"
      tranCode: "SESSIONS_DEPOSIT"
      params: { account: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b", amount: "250.00" }
    }
  ) {
    transactionId
  }
}
```

Read the session's latest turn. You will run this query again in later steps.

```graphql
query LatestTurn {
  actions {
    session(
      actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      id: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
    ) {
      status
      reason
      turns(first: 1) {
        nodes {
          number
          status
          final
          inputs {
            kind
            triggers
            event {
              eventType
              data
            }
          }
          execution {
            executionId
            status
            invocation {
              output
              approvals {
                interruptId
                tool
                input
              }
              hold {
                interruptId
                cause
                category
                explanation
              }
            }
          }
        }
      }
    }
  }
}
```

```json
{
  "status": "WAITING",
  "reason": "Waiting for approval to call notify_ops.",
  "turns": {
    "nodes": [
      {
        "number": 1,
        "status": "COMMITTED",
        "final": false,
        "inputs": [
          {
            "kind": "EVENT",
            "triggers": [0],
            "event": {
              "eventType": "account.inserted",
              "data": {
                "account_id": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b",
                "code": "SESSIONS.ALICE",
                "metadata": {
                  "watch": true
                },
                "…": "…"
              }
            }
          },
          {
            "kind": "EVENT",
            "triggers": [1],
            "event": {
              "eventType": "entry.inserted",
              "data": {
                "account_id": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b",
                "amount": "250.00 USD",
                "entry_type": "DEPOSIT_CR",
                "…": "…"
              }
            }
          }
        ],
        "execution": {
          "executionId": "33a4520c-0017-537b-88c8-2209b3104bad",
          "status": "SUCCEEDED",
          "invocation": {
            "output": null,
            "approvals": [
              {
                "interruptId": "v1:before_tool_call:tooluse_33a4520c_1_1:128bdd77-c3a5-5902-9bbc-0ab0209465e9",
                "tool": "notify_ops",
                "input": {
                  "account_id": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b",
                  "note": "First deposit received. Please confirm identity checks."
                }
              }
            ],
            "hold": null
          }
        }
      }
    ]
  }
}
```

What to look for:

- **The turn took both inputs** that were waiting: the `account.inserted` event that started the session and the `entry.inserted` event that started the turn, in the order they arrived. `triggers` lists the positions of the triggers each one matched: `0` is `opened` and `1` is `entry`.
- **The turn committed with the session `WAITING`.** The script made the agent call `notify_ops`, which requires approval, so the turn ended before the call and committed everything up to it. It has no `output`.
- **`approvals` lists the paused call**: the tool, the input it will run with (`account_id` is the fixed input, `note` came from the model), and the `interruptId` you answer it with.

Copy the `interruptId` into the Variables panel:

```json
{ "interruptId": "REPLACE_WITH_INTERRUPT_ID" }
```

## 6. Approve the call

```graphql
mutation ApproveNotify($interruptId: String!) {
  actions {
    respondToSession(
      actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      id: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
      interruptId: $interruptId
      response: { approval: { decision: APPROVE, reason: "Identity checks confirmed." } }
    ) {
      identity
      kind
      status
    }
  }
}
```

```json
{
  "identity": "2dac34bd-7b48-5074-8263-08562ad60728",
  "kind": "COMMAND",
  "status": "PENDING"
}
```

The answer is admitted to the session as a `COMMAND` input and runs in a turn of its own, where the agent continues from where it paused: the approved call runs, and the agent replies. Read turn 2 and the call it made:

```graphql
query ApprovedCall {
  actions {
    session(
      actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      id: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
    ) {
      status
      turn(number: 2) {
        status
        inputs {
          kind
          event {
            eventType
            data
          }
        }
        execution {
          invocation {
            output
          }
          children(first: 10) {
            nodes {
              action {
                definition {
                  name
                }
              }
              status
              invocation {
                output
              }
            }
          }
        }
      }
    }
  }
}
```

```json
{
  "status": "OPEN",
  "turn": {
    "status": "COMMITTED",
    "inputs": [
      {
        "kind": "COMMAND",
        "event": {
          "eventType": "action_session.responded",
          "data": {
            "decision": "APPROVE",
            "interrupt_id": "v1:before_tool_call:…",
            "reason": "Identity checks confirmed.",
            "responder": "arn:aws:iam::000000000000:user/twisp",
            "tool": "notify_ops"
          }
        }
      }
    ],
    "execution": {
      "invocation": {
        "output": "Still watching."
      },
      "children": {
        "nodes": [
          {
            "action": {
              "definition": {
                "name": "sessions-notify-ops"
              }
            },
            "status": "SUCCEEDED",
            "invocation": {
              "output": {
                "account_id": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b",
                "note": "First deposit received. Please confirm identity checks.",
                "status": "sent"
              }
            }
          }
        ]
      }
    }
  }
}
```

The session is `OPEN` again. Turn 2's only input is your answer, recorded as an `action_session.responded` event with your decision, reason, and identity. `children` holds the `notify_ops` call as its own execution, with its output, and the turn's `output` is the agent's reply.

## 7. Post a deposit the agent declines to review

Wait until at least a minute has passed since you opened the accounts, so that the session's clock has fired, then post a larger deposit:

```graphql
mutation SecondDeposit {
  postTransaction(
    input: {
      transactionId: "fbad78e1-2ec1-4973-a7d0-6c2e44b41b22"
      tranCode: "SESSIONS_DEPOSIT"
      params: { account: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b", amount: "9000.00" }
    }
  ) {
    transactionId
  }
}
```

Run `LatestTurn` from step 5 again:

```json
{
  "status": "WAITING",
  "reason": "Turn 3 is held: the model declined to continue it. Answer it with RETRY or SKIP.",
  "turns": {
    "nodes": [
      {
        "number": 3,
        "status": "HELD",
        "final": false,
        "inputs": [
          {
            "kind": "EVENT",
            "triggers": [1],
            "event": {
              "eventType": "entry.inserted",
              "data": {
                "account_id": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b",
                "amount": "9000.00 USD",
                "entry_type": "DEPOSIT_CR",
                "…": "…"
              }
            }
          },
          {
            "kind": "SCHEDULE",
            "triggers": [],
            "event": {
              "eventType": "schedule.fired",
              "data": {
                "missed": 0,
                "schedule": "tick",
                "scheduled_at": "2026-10-11T18:04:35.390689Z"
              }
            }
          }
        ],
        "execution": {
          "executionId": "34a580e0-d174-55cb-b8d0-ca2caae55da4",
          "status": "FAILED",
          "invocation": {
            "output": null,
            "approvals": [],
            "hold": {
              "interruptId": "34a580e0-d174-55cb-b8d0-ca2caae55da4",
              "cause": "REFUSED",
              "category": "review",
              "explanation": "A person should look at this deposit first."
            }
          }
        }
      }
    ]
  }
}
```

- **Turn 3 took a firing of the session's clock.** `tick` fired while the session waited, and because `turns.on` names only `entry`, the firing waited for this turn. It is an input of kind `SCHEDULE` whose `schedule.fired` event names the entry and its time. `missed` counts earlier firings of `tick` that the turn passed over.
- **The turn is `HELD`.** The script made the model decline to continue this turn. Instead of failing the session, Twisp discarded everything the turn did and held it: the session is `WAITING`, with a reason naming the turn, and `hold` says why. A turn that never reports an outcome after its retries is held the same way, with cause `UNFINISHED`.

Copy the hold's `interruptId` into the Variables panel.

## 8. Retry the held turn

Answer the hold with `RETRY` and a note for the agent. `SKIP` would instead go on without the held turn's inputs.

```graphql
mutation RetryHeldTurn($interruptId: String!) {
  actions {
    respondToSession(
      actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      id: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
      interruptId: $interruptId
      response: {
        hold: {
          decision: RETRY
          note: "The deposit came from the customer's own bank. Keep watching."
        }
      }
    ) {
      identity
      kind
      status
    }
  }
}
```

Run `LatestTurn` again:

```json
{
  "status": "OPEN",
  "reason": null,
  "turns": {
    "nodes": [
      {
        "number": 4,
        "status": "COMMITTED",
        "final": false,
        "inputs": [
          {
            "kind": "COMMAND",
            "triggers": [],
            "event": {
              "eventType": "action_session.responded",
              "data": {
                "decision": "RETRY",
                "interrupt_id": "34a580e0-d174-55cb-b8d0-ca2caae55da4",
                "note": "The deposit came from the customer's own bank. Keep watching.",
                "responder": "arn:aws:iam::000000000000:user/twisp"
              }
            }
          }
        ],
        "execution": {
          "executionId": "53214c2d-c4e5-5190-8c90-67ba74fbc533",
          "status": "SUCCEEDED",
          "invocation": {
            "output": "Still watching.",
            "approvals": [],
            "hold": null
          }
        }
      }
    ]
  }
}
```

Turn 4 committed and the session is `OPEN`. The turn's record lists your answer as its input, and the turn ran the held inputs again with your note. Its execution's transcript shows exactly what the model was given. Read the first event of the turn's transcript:

```graphql
query RetriedTurnMessage {
  actions {
    session(
      actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      id: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
    ) {
      turn(number: 4) {
        execution {
          invocation {
            transcript(first: 3) {
              nodes {
                sequence
                kind
                data
              }
            }
          }
        }
      }
    }
  }
}
```

The first event, `model.input`, holds the turn's message. Trimmed to the inputs, it reads:

```xml
<turn session="82a7bfe9-d43e-4db9-8c91-efdf40ec822b" step="3" now="2026-10-11T18:05:03.115332Z" started_at="2026-10-11T18:03:35.390689Z" ends_at="2026-10-11T18:18:35.390689Z">
<session_input>{"account_id":"82a7bfe9-d43e-4db9-8c91-efdf40ec822b"}</session_input>
<inputs count="3" inline="3">
<input n="1" identity="8fc3b2a3-…" type="entry.inserted" committed_at="2026-10-11T18:04:46.209108Z" trigger="entry">{"data":{"account_id":"82a7bfe9-d43e-4db9-8c91-efdf40ec822b","amount":"9000.00 USD",…}}</input>
<input n="2" identity="schedule/tick/2026-10-11T18:04:35.390689Z" kind="schedule" source="tick" scheduled_at="2026-10-11T18:04:35.390689Z"/>
<input n="3" identity="41dacf88-…" kind="command" type="action_session.responded" committed_at="2026-10-11T18:05:02.83208Z">{"data":{"decision":"RETRY","note":"The deposit came from the customer's own bank. Keep watching.",…}}</input>
</inputs>
</turn>
```

The agent saw the held deposit, the clock's firing, and your note. `step` is 3 because the held turn counts as one of the session's turns.

## 9. Close a session

Bob's account needs no watching after all. Close his session with a reason:

```graphql
mutation CloseBobSession {
  actions {
    closeSession(
      actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      id: "5a1933de-63f9-44ff-83aa-24987e2d9c39"
      reason: "Onboarding team reviewed this account."
    ) {
      sessionId
      status
      reason
    }
  }
}
```

```json
{
  "sessionId": "5a1933de-63f9-44ff-83aa-24987e2d9c39",
  "status": "CANCELLED",
  "reason": "Onboarding team reviewed this account."
}
```

A closed session accepts no further input: later entries on Bob's account are not delivered to it. Its inputs and turns stay readable, and closing it again returns it unchanged. List both sessions:

```graphql
query AllWatchSessions {
  actions {
    sessions(where: { actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0" }, first: 10) {
      nodes {
        sessionId
        status
        reason
      }
    }
  }
}
```

```json
{
  "nodes": [
    {
      "sessionId": "82a7bfe9-d43e-4db9-8c91-efdf40ec822b",
      "status": "OPEN",
      "reason": null
    },
    {
      "sessionId": "5a1933de-63f9-44ff-83aa-24987e2d9c39",
      "status": "CANCELLED",
      "reason": "Onboarding team reviewed this account."
    }
  ]
}
```

## 10. Watch the session end

Fifteen minutes after Alice's account opened, her session reaches its `endsAt`. Firings of `tick` that arrived since turn 4 are pending, so the session gets a final turn at once, which takes everything pending. The script's final-turn entries reply and complete the session. If nothing were pending, the session would complete with no turn, with reason `ended`.

After `endsAt` has passed, read the session and all its turns:

```graphql
query WatchEnded {
  actions {
    session(
      actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0"
      id: "82a7bfe9-d43e-4db9-8c91-efdf40ec822b"
    ) {
      status
      reason
      endsAt
      turns(first: 10) {
        nodes {
          number
          status
          final
          created
          inputs {
            kind
            event {
              eventType
              data
            }
          }
          execution {
            invocation {
              output
            }
          }
        }
      }
    }
  }
}
```

```json
{
  "status": "COMPLETED",
  "reason": "Watch period ended with no findings.",
  "endsAt": "2026-10-11T18:18:35.390689Z",
  "turns": {
    "nodes": [
      {
        "number": 5,
        "status": "COMMITTED",
        "final": true,
        "created": "2026-10-11T18:18:36.182351Z",
        "inputs": [
          {
            "kind": "SCHEDULE",
            "event": {
              "eventType": "schedule.fired",
              "data": {
                "missed": 12,
                "schedule": "tick",
                "scheduled_at": "2026-10-11T18:17:35.390689Z"
              }
            }
          }
        ],
        "execution": {
          "invocation": {
            "output": "Watch period over."
          }
        }
      },
      {
        "number": 4,
        "status": "COMMITTED",
        "final": false,
        "created": "2026-10-11T18:05:02.966275Z",
        "inputs": [
          {
            "kind": "COMMAND",
            "event": {
              "eventType": "action_session.responded",
              "data": {
                "…": "…"
              }
            }
          }
        ],
        "execution": {
          "invocation": {
            "output": "Still watching."
          }
        }
      },
      {
        "number": 3,
        "status": "HELD",
        "final": false,
        "created": "2026-10-11T18:04:46.665396Z",
        "inputs": [
          {
            "kind": "EVENT",
            "event": {
              "eventType": "entry.inserted",
              "data": {
                "…": "…"
              }
            }
          },
          {
            "kind": "SCHEDULE",
            "event": {
              "eventType": "schedule.fired",
              "data": {
                "missed": 0,
                "schedule": "tick",
                "scheduled_at": "2026-10-11T18:04:35.390689Z"
              }
            }
          }
        ],
        "execution": {
          "invocation": {
            "output": null
          }
        }
      },
      {
        "number": 2,
        "status": "COMMITTED",
        "final": false,
        "created": "2026-10-11T18:04:02.866639Z",
        "inputs": [
          {
            "kind": "COMMAND",
            "event": {
              "eventType": "action_session.responded",
              "data": {
                "…": "…"
              }
            }
          }
        ],
        "execution": {
          "invocation": {
            "output": "Still watching."
          }
        }
      },
      {
        "number": 1,
        "status": "COMMITTED",
        "final": false,
        "created": "2026-10-11T18:03:46.772632Z",
        "inputs": [
          {
            "kind": "EVENT",
            "event": {
              "eventType": "account.inserted",
              "data": {
                "…": "…"
              }
            }
          },
          {
            "kind": "EVENT",
            "event": {
              "eventType": "entry.inserted",
              "data": {
                "…": "…"
              }
            }
          }
        ],
        "execution": {
          "invocation": {
            "output": null
          }
        }
      }
    ]
  }
}
```

Turn 5 is `final`. It took only the latest `tick` firing, and `missed: 12` counts the earlier firings since turn 4 that it passed over. The session is `COMPLETED` with the reason the final turn reported. Firings stop at the end, and later entries on Alice's account are not delivered.

## Troubleshooting

| Symptom                                     | Check                                                                                                                                                                                          |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No session appears after opening an account | The agent must be `status: ACTIVE` before the account is inserted, and the account needs `metadata: { watch: true }`. Open a new account with a new ID and code.                               |
| More sessions than you opened               | Every active action with a matching start trigger starts its own session. Disable agents left from earlier runs of this tutorial.                                                              |
| The deposit starts no turn                  | The entry must be on a watched account whose session is open. An entry that arrives while the session is `WAITING` is delivered once it is `OPEN` again.                                       |
| `respondToSession` fails                    | The session must be `WAITING` on that `interruptId`, and each request is answered once. Read `approvals` or `hold` from the latest turn again.                                                 |
| Turn 3 has no `SCHEDULE` input              | Less than a minute had passed since the session started. The firing waits for the next turn instead.                                                                                           |
| The approval turn or turn 3 is `final`      | The session's end passed before that step. The final turn completes the session, so later steps have nothing to resume. Start again with a new agent and accounts, or lengthen `session.ends`. |
| A turn does not do what the step describes  | The script keys on `session.step`, so the order of turns matters: post the deposits and answer the requests in the order shown, each after the turn before it has committed.                   |

Read [Sessions](https://www.twisp.com/docs/actions/sessions.md) for when to give an agent a session, and the [Sessions reference](https://www.twisp.com/docs/reference/actions/sessions.md) for [session roles](https://www.twisp.com/docs/reference/actions/sessions.md#session-roles), [turns](https://www.twisp.com/docs/reference/actions/sessions.md#session-turns), the [end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end) and [clock](https://www.twisp.com/docs/reference/actions/sessions.md#session-clock), [approvals](https://www.twisp.com/docs/reference/actions/sessions.md#approvals), and [held turns](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns). The [fixture model](https://www.twisp.com/docs/reference/actions/agents.md#fixture-model) and the [turn outcome](https://www.twisp.com/docs/reference/actions/agents.md#turn-outcome) are in the Agents reference.
