# Sessions

Give an action a durable identity across many events, with ordered turns, an end, a clock, approvals, and held turns.

Without a session, each execution of an action stands alone: it sees one event and remembers nothing. Some work is about one subject over time instead. Watching a newly opened account for its first 30 days means reviewing every entry posted to that account, in order, knowing what was already reviewed and which findings are open, and stopping when the 30 days are over.

A **session** gives an action that continuity. Each session has an ID, an input fixed when it starts, state that the action carries from turn to turn, an ordered history of everything delivered to it, and a lifecycle. The same action and session ID always address the same session.

> **Note:**
>
> The [Sessions reference](https://www.twisp.com/docs/reference/actions/sessions.md) lists every field and rule. This page uses one running example: an `early-fraud-detector` agent that watches each new account for 30 days.

## The session block

An action's `session` block declares how its sessions are identified, when they end, and the clock they keep; its triggers declare which events start or reach a session.

```graphql
triggers: [
  {
    name: "opened"
    event: "event.type == \"account.inserted\""
    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", format: "uuid" } }
    required: ["account_id"]
  }
  id: "string(session.input.account_id)"
  ends: "session.started_at + duration('720h')"
  schedule: [{ name: "morning", cron: "0 9 * * 1-5", timezone: "America/New_York" }]
  turns: { on: ["entry", "morning"] }
}
```

| Field         | What it decides                                                                                              |
| ------------- | ------------------------------------------------------------------------------------------------------------ |
| `inputSchema` | The shape of each session's `input`, fixed when it starts. Here, the account it watches.                     |
| `id`          | The session ID, computed from the input. One account, one session.                                           |
| `ends`        | When each session ends. Here, 720 hours after it started. See [The end of a session](https://www.twisp.com/docs/actions/sessions.md#the-end-of-a-session). |
| `schedule`    | The session's own clock. See [Schedules](https://www.twisp.com/docs/actions/sessions.md#schedules).                                                        |
| `turns`       | When the session works. See [Turns](https://www.twisp.com/docs/actions/sessions.md#turns).                                                                 |

Each session keeps the action version it started with. Updating the definition changes the triggers, schemas, tools, end, and clock of sessions started after the update, never of those already open.

## Session roles

With a `session` block, every trigger has a [session role](https://www.twisp.com/docs/reference/actions/sessions.md#session-roles): it either starts a session or resumes one.

| Role              | What a matching event does                                                                                                                             |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `start: {input}`  | Maps the event to the session's input, computes the ID, and finds the open session with that ID or creates it. A closed session ID never starts again. |
| `resume: {id}`    | Delivers the event to the open session with that ID. It never creates a session.                                                                       |
| `resume: {input}` | Delivers the event to every open session whose input has a field equal to a value from the event, such as every session watching one customer.         |

In the example, `account.inserted` starts the account's session, and each `entry.inserted` for that account resumes it. An entry that arrives before its account's session exists waits up to 24 hours for the session to start.

An event that fails to start or resume a session, such as one whose mapped input fails `inputSchema`, runs nothing. It is kept as an [intake record](https://www.twisp.com/docs/reference/actions/executions.md#intake-records): a failed execution that names the trigger and the step that failed. Fix the definition, then rerun the record to deliver the event.

## Turns

A session works in **turns**. A turn is one pass of the action's handler over a set of the session's pending inputs; for an agent, it is the agent's whole loop up to its outcome. Turns run one at a time, and each commits its new state, its output, and the consumption of its inputs together, so every turn builds on what the turn before committed. See [Session turns](https://www.twisp.com/docs/reference/actions/sessions.md#session-turns).

Events delivered to a session wait in it, in the order they arrived, until a turn takes them. `session.turns` decides when that happens:

| Setting                 | Use it to                                                                                                                                                                                                                                                |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `on`                    | Name the triggers and schedule entries whose inputs start a turn. Other inputs wait for the next turn. In the example, the opening itself starts no turn: the first entry, or the next `morning` firing, starts a turn that reviews the opening with it. |
| `window`                | After the session has been idle, wait this long after the first input before starting a turn, so a burst of entries is one turn instead of many.                                                                                                         |
| `maxInputs`, `maxBytes` | Bound how much one turn takes. An agent's turn takes every pending input up to 1,000 by default; a Starlark action's, one.                                                                                                                               |

An agent's turn shows each input to the model whole by default. A trigger's [view](https://www.twisp.com/docs/reference/actions/agents.md#input-views) shows only the fields the agent needs, such as an entry's amount, direction, and time, and the agent can still read the whole event.

## The lifecycle

```text
 account.inserted
        |
        v
   +---------+   turn reports OPEN    +---------+
   |  OPEN   | <--------------------- |  turn   |
   |         | ---------------------> | running |
   +---------+   input starts a turn  +---------+
     |   ^                                 |  |
     |   |  respondToSession               |  +--> COMPLETED  (final turn, or a turn
     |   |                                 |                   that completes it)
     |   +------------ WAITING <-----------+  approval needed, or turn held
     |                                     |
     |                                     +-----> FAILED     (turn failed for good)
     |
     +--> COMPLETED, reason "ended"   (end reached, nothing pending)
     +--> CANCELLED                   (closeSession, from any open status)
```

| Session status | Meaning                                                                                                                                             | Final |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| `OPEN`         | Accepting inputs. A turn runs when an input starts one.                                                                                             | no    |
| `WAITING`      | Waiting for a person: an [approval](https://www.twisp.com/docs/actions/sessions.md#approvals) or a [held turn](https://www.twisp.com/docs/actions/sessions.md#held-turns). New inputs wait, in order.                                           | no    |
| `COMPLETED`    | A turn completed it, or its end passed with nothing pending.                                                                                        | yes   |
| `FAILED`       | A turn failed for good: after its retries, or with an error that is not retried. Its inputs remain, unconsumed, and the reason names the execution. | yes   |
| `CANCELLED`    | An operator closed it.                                                                                                                              | yes   |

No input is delivered to a session in a final status. Each turn has a status of its own: `RUNNING`, `COMMITTED`, `FAILED`, `HELD`, or `CANCELLED`.

## The end of a session

`session.ends` makes "30 days" a rule the platform enforces rather than an instruction to the model. It is evaluated when the session starts, and the result is the session's `endsAt`. See [Session end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end).

- **Before the end**, no turn may complete the session. An agent is offered only `OPEN`.
- **At the end**, a session with pending inputs gets a **final turn** at once. It takes every pending input, whatever `turns.on` names, and must complete the session with a reason.
- **With nothing pending**, the session completes with the reason `ended`, and no turn runs. A quiet account costs nothing.
- **Nothing goes unreviewed.** A turn is final only when it takes every pending input; if `maxInputs` leaves some, the next turn continues.

Without `ends`, a session completes only when a turn reports `COMPLETED` or an operator closes it.

## Schedules

`session.schedule` gives each session its own clock: up to 8 named entries, each a `cron` expression in a timezone or an `every` period from the session's start. In the example, the `morning` entry gives each account's session a weekday review at 9:00 in New York, even on a day with no entries. See [Session clock](https://www.twisp.com/docs/reference/actions/sessions.md#session-clock).

A firing is an input to that session alone, of kind `SCHEDULE`. It starts a turn at once when `turns.on` names its entry or is omitted, and never waits for a `window`. A turn takes at most one firing of each entry, the latest due, and `missed` counts the firings it passed over, for example while the session was waiting for an approval. No entry fires at or after the session's end.

Use a [schedule trigger](https://www.twisp.com/docs/reference/actions/actions.md#schedule-triggers), not a session schedule, for work that is about no particular subject, such as a nightly report.

## Approvals

Some tool calls should not happen without a person. Give a tool reference `approval: REQUIRED`, and before each call to it the agent's turn commits with the session `WAITING`. Everything the turn did before the call commits with it. See [Approvals](https://www.twisp.com/docs/reference/actions/sessions.md#approvals).

```graphql
tools: [
  { name: "get_account_activity", input: { account_id: "string(session.input.account_id)" } }
  { name: "notify_human_ops", approval: REQUIRED }
]
```

The turn's execution lists the paused call in `invocation.approvals`, with the tool, the version it would run, and its input. Answer with `respondToSession` and `response.approval`: `APPROVE`, optionally with replacement input, or `DENY`, each with an optional reason. The answer is delivered as a `COMMAND` input in a turn of its own, and the agent continues from where it paused: an approved call runs, and a denied call is reported to the agent with the reason. Answering requires the `RESPOND` policy action on the agent's action record, and the answer is recorded with the responder.

Only an agent with a session can require approval. A gated tool cannot be called from programmatic tool calling. In [preview mode](https://www.twisp.com/docs/reference/actions/executions.md#preview-calls), a gated call runs at once as if approved, and is marked `grantedInPreview` so the record shows no person approved it.

## Held turns

A turn that the model declines to continue, or that never reports a valid outcome, does not fail the session. It is **held**: nothing it did commits, its transcript is kept, and the session waits with the state the turn before left. See [Held turns](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns).

The turn's execution has `invocation.hold`, with the `cause`, `REFUSED` or `UNFINISHED`, and, for a refusal, what the provider reported. Read its transcript, then answer with `respondToSession` and `response.hold`:

- **`RETRY`** runs the held turn's inputs again, with your optional `note` shown to the agent.
- **`SKIP`** goes on without them, with no model call. A turn that resumed from approval responses cannot be skipped, since the responses cannot be given again: retry it, or close the session.

A retried turn runs at the session's pinned version, so fixing the agent's definition helps sessions started after the fix, not the held one.

## Inspecting and closing a session

A session reaches everything that happened in it: its `state`, its `turns` with each turn's inputs, checkpoint, and execution, its `inputs` in delivery order, and its `history` of versions. Read one with `session(actionId, id)`, or list them with `sessions(where: ...)`. See [Inspecting and closing sessions](https://www.twisp.com/docs/reference/actions/sessions.md#inspecting-and-closing-sessions).

### Closing a session

`closeSession(actionId, id, reason)` cancels one session. It accepts no further input, a running turn cannot commit, and its history stays readable. Effects already performed are not undone. Close a session to stop one subject's work without touching the others; disable the action to stop new sessions.

## Tool sessions

A tool can have a session too. A [tool session](https://www.twisp.com/docs/reference/actions/sessions.md#tool-sessions) is call history only: each call names its session with a `start` mapping over `tool.input`, and each committed call adds one `CALL` input to it, with its input and output. It has no state, turns, end, or schedule, and calls never wait for one another. Use it when a tool should see its earlier calls for the same subject, such as a notification tool that must not notify twice about one finding.

```graphql
tool: {
  name: "notify_human_ops"
  description: "Notify human operations about a finding."
  inputSchema: {
    type: "object"
    properties: { account_id: { type: "string" }, note: { type: "string" } }
    required: ["account_id"]
  }
  session: { start: { input: { account_id: "tool.input.account_id" } } }
}
```

An [MCP server](https://www.twisp.com/docs/actions/mcp-servers.md) records every request in a session of its own, grouped by user, host, or your run ID; see [MCP sessions](https://www.twisp.com/docs/reference/actions/mcp.md#sessions).

## Next

- [Memory](https://www.twisp.com/docs/actions/memory.md): Keep facts beyond a session, and let a session search its own earlier turns.
- [Sessions tutorial](https://www.twisp.com/docs/tutorials/actions/sessions.md): Build the 30-day account watch, step by step.
- [Sessions reference](https://www.twisp.com/docs/reference/actions/sessions.md): Every field, rule, and operation.
