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

# Logging

> See what your agent did, per run: ctx.log records structured logs that Duraton captures, keeps durable under replay, and shows against the run.

`ctx.log` records structured, leveled logs from inside a workflow. Unlike a bare `console.log` - which
stays on the runner's stdout, invisible to Duraton - `ctx.log` lines flow to Duraton, persist
durably with the run, and are readable per run via the [API](/reference/api/runs#run-logs).

## Logging a line

`ctx.log` is callable (info level) and has one method per level:

```ts theme={null}
const ticketCreated = workflow<TicketData>({
  name: "ticket.created",
  handler: async (ctx) => {
    ctx.log.info("ticket received", { ticketId: ctx.event.data.ticketId });

    const triage = await ctx.step.run("triage", async () => {
      ctx.log.info("triaging ticket", { priority: "high" });
      return triageTicket(ctx.event.data);
    });

    ctx.log.warn("triaged, refunding next", { refundId: triage.id });
  },
});
```

| Call                              | Level   |
| --------------------------------- | ------- |
| `ctx.log(message, fields?)`       | `info`  |
| `ctx.log.debug(message, fields?)` | `debug` |
| `ctx.log.info(message, fields?)`  | `info`  |
| `ctx.log.warn(message, fields?)`  | `warn`  |
| `ctx.log.error(message, fields?)` | `error` |

`fields` is an optional object of structured context. It is stored as JSON, so prefer structured
fields over interpolating values into the message.

## Durable under replay

A handler re-runs from the top on every pass ([durable
execution](/core/durable-execution)), so `ctx.log` is replay-aware:

* A **handler-level** log (outside any step) re-emits on every pass, but Duraton gives it a stable
  identity per attempt and records it **exactly once**.
* A log **inside a `step.run`** only executes on the pass where that step runs. A step that **retries**
  records its logs **once per attempt**, so you can see what each attempt did:

```ts theme={null}
await ctx.step.run("call-upstream", async () => {
  ctx.log.info("calling upstream", { attempt: ctx.attempt });
  const res = await callUpstream();
  if (!res.ok) {
    ctx.log.warn("upstream failed, will retry");
    throw new Error("upstream error");
  }
  return res;
});
```

## Redaction

Field values under sensitive key names (`password`, `token`, `secret`, `authorization`, and similar)
are masked to `[redacted]` before anything is persisted.

Redaction is keyed on field **names**, so prefer putting sensitive values in named fields rather than
inlining them into the free-text `message`.

## Reading logs back

Fetch a run's logs oldest-first:

```sh theme={null}
curl "$DURATON_URL/runs/<id>/logs"
```

Each line carries its `level`, `message`, `fields`, `scope` (the step name, or `@root` for a
handler-level log), and `attempt`. See the [Runs API](/reference/api/runs#run-logs) for pagination and the
full response shape.

## Limits

Each pass caps how many lines it ships so a pathologically chatty handler cannot overrun the 1 MiB
wire-message limit; beyond the cap, a single line records how many were dropped. Logs are part of a
run's data and are removed with the run.

<Note>
  See the **logging** example running end to end in [Examples](/start/recipes#log-then-watch-a-run-live).
</Note>
