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.

You need a running local environment. 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.

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

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:

PartWhat it does
opened triggerA 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 triggerA 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.idEach session's ID is its account ID, so the same account always addresses the same session.
session.endsEach 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.scheduleThe session's own clock: tick fires every minute after the session starts, as an input to that session alone.
turns.onOnly entry inputs start a turn. The account event and tick firings wait and ride along with the next turn.
toolsThe 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.scriptThe 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.
harnessbuiltin_tools: [] turns off the agent tools, which this agent does not need.

Poll until the agent is live:

query ReadAccountWatch {
  actions {
    action(id: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0") {
      status
      pending
      definition {
        session {
          id
          ends
          schedule {
            name
            every
          }
          turns {
            on
          }
        }
      }
    }
  }
}
{
  "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"]
      }
    }
  }
}

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.

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.

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:

query ListSessions {
  actions {
    action(id: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0") {
      sessions(first: 10) {
        nodes {
          sessionId
          status
          input
          startedAt
          endsAt
          turns(first: 1) {
            nodes {
              number
            }
          }
        }
      }
    }
  }
}
{
  "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.

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.

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
              }
            }
          }
        }
      }
    }
  }
}
{
  "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:

{ "interruptId": "REPLACE_WITH_INTERRUPT_ID" }

6. Approve the call

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
    }
  }
}
{
  "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:

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
              }
            }
          }
        }
      }
    }
  }
}
{
  "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:

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:

{
  "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.

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:

{
  "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:

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:

<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:

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
    }
  }
}
{
  "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:

query AllWatchSessions {
  actions {
    sessions(where: { actionId: "2c41d6a5-4ac8-4701-882d-bb5b6fd039d0" }, first: 10) {
      nodes {
        sessionId
        status
        reason
      }
    }
  }
}
{
  "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:

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
            }
          }
        }
      }
    }
  }
}
{
  "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

SymptomCheck
No session appears after opening an accountThe 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 openedEvery active action with a matching start trigger starts its own session. Disable agents left from earlier runs of this tutorial.
The deposit starts no turnThe 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 failsThe 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 inputLess than a minute had passed since the session started. The firing waits for the next turn instead.
The approval turn or turn 3 is finalThe 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 describesThe 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 for when to give an agent a session, and the Sessions reference for session roles, turns, the end and clock, approvals, and held turns. The fixture model and the turn outcome are in the Agents reference.

Previous
Your First Agent