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 });
},
});| 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 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.runonly 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.