Concepts

Logging

Record structured logs from workflow code with ctx.log - captured by the engine, durable under replay, and readable per 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 the engine - ctx.log lines flow to the engine, 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 orderCreated = defineWorkflow<OrderData>({
  name: "order.created",
  handler: async (ctx) => {
    ctx.log.info("order received", { orderId: ctx.event.data.orderId });

    const charge = await ctx.step.run("charge", async () => {
      ctx.log.info("charging card", { amount: 4200 });
      return chargeCard(ctx.event.data);
    });

    ctx.log.warn("charged, shipping next", { chargeId: charge.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 workflow handler re-runs from the top on every pass (that is what makes durable execution work). A naive logger would therefore record a top-level line many times. ctx.log is replay-aware:

  • A handler-level log (outside any step) re-emits on every pass, but the engine gives it a stable identity per attempt and records it exactly once.
  • A log inside a step.run only runs on the pass where that step executes, so it is recorded when the step actually 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;
});

You never have to guard logging behind a "first run only" check - that is handled for you.

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 "$DURABLEX_ENGINE_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.

On this page