> ## Documentation Index
> Fetch the complete documentation index at: https://paymanai.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Actions

> Run an instruction, handle one of four results.

```ts theme={null}
const result = await payman.forUser(req.session.userId).run(
  "Pay the Acme invoice for $120",
);

switch (result.kind) {
  case "ok":               // done: render result.message
    break;
  case "needs_connection": // render the connect button
    break;
  case "approval":         // money stopped for the customer
    break;
  case "error":            // show result.guidance
    break;
}
```

One instruction in, one result out. `run()` resolves one of four results and never throws for anything that fails remotely; those failures arrive as the `error` result. Throwing is reserved for your own configuration or store faults.

Call `forUser()` inside the request handler, once per call. Never hoist it to module scope.

## The four results

| `result.kind`      | When                                         | What your app does                                                |
| ------------------ | -------------------------------------------- | ----------------------------------------------------------------- |
| `ok`               | The action completed                         | Render `message`, attributed to the customer's Payman account.    |
| `needs_connection` | No connection is held, or it stopped working | Render the connect button. The instruction is stashed for replay. |
| `approval`         | The action froze for the customer's approval | Render the approval button. See [Approvals](/build/approvals).    |
| `error`            | Anything else                                | Show `guidance`.                                                  |

### What each result carries

<Tabs>
  <Tab title="ok">
    | Field         | Type   | Meaning                                          |
    | ------------- | ------ | ------------------------------------------------ |
    | `message`     | string | The reply, verbatim. An empty reply stays empty. |
    | `agentStatus` | string | The run's final status string.                   |
  </Tab>

  <Tab title="needs_connection">
    | Field        | Type                                    | Meaning                                                       |
    | ------------ | --------------------------------------- | ------------------------------------------------------------- |
    | `reason`     | `"no_grant"` \| `"credential_rejected"` | This user never connected, or the connection stopped working. |
    | `connectUrl` | string                                  | The consent URL, ready to render as a button.                 |
    | `stashed`    | boolean                                 | True when the instruction was kept for replay after consent.  |
  </Tab>

  <Tab title="approval">
    | Field                | Type                                    | Meaning                                                                    |
    | -------------------- | --------------------------------------- | -------------------------------------------------------------------------- |
    | `approval.ref`       | string                                  | Opaque handle for the frozen operation. Safe to show a model.              |
    | `approval.channel`   | `"payman_hosted"` \| `"payman_console"` | Where the customer approves.                                               |
    | `approval.url`       | string \| null                          | Payman's approval page. Null exactly when the channel is `payman_console`. |
    | `approval.expiresAt` | Date \| null                            | When the approval lapses, about 15 minutes out.                            |
    | `message`            | string \| null                          | Text produced alongside the freeze, if any.                                |
  </Tab>

  <Tab title="error">
    | Field          | Type    | Meaning                                                                                         |
    | -------------- | ------- | ----------------------------------------------------------------------------------------------- |
    | `code`         | string  | Stable identifier to switch on. Full list in [Limits and errors](/reference/limits-and-errors). |
    | `retryable`    | boolean | Worth one more attempt.                                                                         |
    | `retryAfterMs` | number? | Wait this long first, when present.                                                             |
    | `guidance`     | string  | Customer-facing prose. Render it as is.                                                         |
  </Tab>
</Tabs>

On the blocking route the SDK retries a retryable failure once before resolving `error`; streamed turns (`runStream()`, or `run()` with `onEvent`) resolve without a retry. Treat `retryable` as permission for one more attempt, not a loop. Retrying never silently re-sends money: anything that moves it stops for the customer's approval first.

### What no result carries

Diagnostics such as internal ids, token usage, and raw payloads sit on a non-enumerable symbol, not on the result's fields. `JSON.stringify(result)` never includes them, so a result is safe to send to the browser as is.

## Sessions

Actions for the same user share one conversation. The SDK mints a session when the connection is stored and continues it on every call; there is nothing to thread through your code.

```ts theme={null}
await payman.forUser(userId).newConversation();
```

`newConversation()` starts a fresh thread. It keeps the connection, clears any stashed instruction, and retires stored approval cards. A fresh chat is not a fresh consent.

## Streaming

```ts theme={null}
// Progress on the same call: setting onEvent switches run() to streaming.
const result = await payman.forUser(userId).run(text, {
  onEvent: (event) => {
    if (event.type === "progress") {
      setStatus(event.progress.label);   // "Looking up recipients..."
    }
  },
});

// Or iterate the events yourself.
const stream = payman.forUser(userId).runStream(text);
for await (const event of stream) {
  if (event.type === "text") append(event.delta);
}
const final = await stream.result();     // the same four results
```

`onEvent` implies streaming: the blocking path emits no events, so setting a listener switches the call over. An explicit `stream: false` wins, and the listener then hears nothing.

The listener receives every event while `run()` still resolves the ordinary result, and a listener that throws never breaks the action. `handleToolUse()` accepts the same option.

| `event.type`  | Carries    | Meaning                                                                    |
| ------------- | ---------- | -------------------------------------------------------------------------- |
| `text`        | `delta`    | The reply as it forms. Append in order.                                    |
| `progress`    | `progress` | What the action is doing right now.                                        |
| `elicitation` | `prompt`   | A choice question for the customer. Answer with `prompt.answer(optionId)`. |
| `approval`    | `approval` | The action froze for approval. Same shape as the `approval` result.        |
| `done`        | `result`   | The final result, identical to `result()`.                                 |

A progress frame is a phase and a label, nothing more. `started` and `completed` bracket one step, so replace the current line instead of stacking; `waiting` is a heartbeat. The label is finished customer-facing copy; render it unchanged.

A stream is consumed once; iterating it twice throws. `result()` and the `done` event carry the identical result.
