Concepts

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.

Logging a line

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

const ticketCreated = defineWorkflow<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 });
  },
});
CallLevel
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), 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:
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:

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

See the logging example running end to end in Examples.

On this page