NOUKAI

Flow Run Trace API

List historical flow runs, fetch per-block input/output payloads, and live-tail an in-progress run over SSE.

Base URL: https://api.noukai.xyz/api/v1

When you execute a flow via /execute or /jobs, Noukai can persist each block's input and output payloads alongside the metadata it already tracks (status, duration, tokens, cost). The Flow Run Trace API exposes those records so you can:

  • Audit historical runs and inspect what each block actually saw and produced.
  • Drive a debugger UI that walks block-by-block through a completed run.
  • Live-tail an in-progress run over Server-Sent Events.

Per-block payload capture is opt-in. With the default metadata_only capture mode, the trace endpoints return timing, token, and cost metadata but inputContext / outputContext come back null and only inputSizeBytes / outputSizeBytes are populated. Switch the flow or organization to full (or redacted) capture to receive payloads. See Capture Modes.

Endpoints

MethodPathPurpose
GET/flow-runsList runs for a flow
GET/flow-runs/{flowRunId}/traceWhole-run trace (latest attempt per step)
GET/flow-runs/{flowRunId}/steps/{stepId}/traceSingle-step trace, with attempt filter
GET/flow-runs/{flowRunId}/trace/streamSSE live tail (replay + follow)

Authentication

Authorization: Bearer nk_live_...

Same auth as the rest of the API — API keys or JWT tokens. The caller must have access to the project that owns the flow; otherwise 403.

List Runs

GET /flow-runs?flow_id={flowId}

Returns the most recent runs for a flow, newest first.

Query Parameters

ParameterTypeRequiredDescription
flow_idstringYesFlow ID to list runs for.
limitintegerNoMax records to return. 1100, default 20.
statusstringNoFilter by run status: running, completed, failed, cancelled.
cursorstringNoReserved for future pagination. Currently ignored.

Response (200)

{
  "runs": [
    {
      "id": "fr_01HX...",
      "flowId": "fl_01HX...",
      "status": "completed",
      "triggerType": "api",
      "startedAt": "2026-05-15T10:23:04.120Z",
      "completedAt": "2026-05-15T10:23:08.941Z",
      "durationMs": 4821,
      "stepCount": 4
    }
  ],
  "nextCursor": null
}
FieldTypeDescription
idstringFlow run ID. Use this in the trace fetches below.
flowIdstringThe flow this run belongs to.
statusstringrunning, completed, failed, or cancelled.
triggerTypestring | nullHow the run started. Free-form string set by the trigger source (e.g. api, test). Treat unknown values as opaque.
startedAtstring | nullISO 8601 timestamp. null until the run is dispatched.
completedAtstring | nullISO 8601 timestamp. null while running.
durationMsinteger | nullEnd-to-end wall time. null while running.
stepCountinteger | nullNumber of step runs recorded for this flow run.

Whole-Run Trace

GET /flow-runs/{flowRunId}/trace

Returns the flow run summary plus the latest attempt of every step. Retries collapse to just the most recent attempt; the only signal in this response that a step retried is attempt > 1. To see prior attempts, call the single-step endpoint with attempt=all.

Path Parameters

ParameterDescription
flowRunIdFlow run ID returned by List Runs or by /execute / /jobs.

Response (200)

{
  "flowRun": {
    "id": "fr_01HX...",
    "flowId": "fl_01HX...",
    "status": "completed",
    "triggerType": "api",
    "startedAt": "2026-05-15T10:23:04.120Z",
    "completedAt": "2026-05-15T10:23:08.941Z",
    "durationMs": 4821,
    "stepCount": 4
  },
  "steps": [
    {
      "stepId": "summarize_essay",
      "attempt": 1,
      "status": "completed",
      "startedAt": "2026-05-15T10:23:04.120Z",
      "completedAt": "2026-05-15T10:23:05.960Z",
      "durationMs": 1840,
      "modelUsed": "openai/gpt-4o-mini",
      "tokens": { "prompt": 412, "completion": 88, "total": 500 },
      "costUsd": "0.00041",
      "inputContext": {
        "__pipeline_input__": { "essay_text": "The industrial revolution..." },
        "config": { "max_length": 200 }
      },
      "outputContext": {
        "summary": "The essay argues that industrialization..."
      },
      "errorContext": null,
      "inputSizeBytes": 1284,
      "outputSizeBytes": 412,
      "truncated": false
    }
  ]
}

Step Trace Object

FieldTypeDescription
stepIdstringStep identifier within the flow. Stable across attempts.
attemptinteger1-based retry counter.
statusstringrunning, completed, failed, or skipped.
startedAtstring | nullISO 8601. null for skipped steps.
completedAtstring | nullISO 8601. null while running.
durationMsinteger | nullWall time. null while running.
modelUsedstring | nullResolved model slug (e.g. openai/gpt-4o-mini) if the block invoked an LLM.
tokensobject | null{ prompt, completion, total }. null for non-LLM blocks or steps that didn't consume tokens.
costUsdstring | nullCost in USD, serialized as a decimal string (e.g. "0.00041") to preserve precision. Parse on the client only when arithmetic is needed. null when the step wasn't metered.
inputContextobject | nullThe block's resolved input payload. null when capture mode is metadata_only / off.
outputContextobject | nullThe block's output payload. null for failed steps, in-flight steps, or metadata_only capture.
errorContextobject | nullPresent only on status: "failed". Shape: { "code": string, "message": string, "retryable": boolean }.
inputSizeBytesinteger | nullByte size of the input payload before any truncation. Populated even when inputContext is null.
outputSizeBytesinteger | nullByte size of the output payload.
truncatedbooleantrue if either payload exceeded the size cap and was truncated. The stored payload then carries a __truncated__: true marker at its root.

Single-Step Trace

GET /flow-runs/{flowRunId}/steps/{stepId}/trace

Fetch one step's trace, with control over which attempt(s) are returned.

Path Parameters

ParameterDescription
flowRunIdFlow run ID.
stepIdStep identifier within the flow.

Query Parameters

ParameterTypeDefaultDescription
attemptstringlatestlatest returns the most recent attempt as a single object. all returns every attempt as a list. A numeric string (e.g. 1, 2) returns that specific attempt.

Response — attempt=latest or a numeric value (200)

Returns a single Step Trace Object:

{
  "stepId": "summarize_essay",
  "attempt": 1,
  "status": "completed",
  "startedAt": "2026-05-15T10:23:04.120Z",
  "completedAt": "2026-05-15T10:23:05.960Z",
  "durationMs": 1840,
  "modelUsed": "openai/gpt-4o-mini",
  "tokens": { "prompt": 412, "completion": 88, "total": 500 },
  "costUsd": "0.00041",
  "inputContext": { "...": "..." },
  "outputContext": { "...": "..." },
  "errorContext": null,
  "inputSizeBytes": 1284,
  "outputSizeBytes": 412,
  "truncated": false
}

Response — attempt=all (200)

{
  "stepId": "summarize_essay",
  "attempts": [
    { "stepId": "summarize_essay", "attempt": 1, "status": "failed",    "...": "..." },
    { "stepId": "summarize_essay", "attempt": 2, "status": "completed", "...": "..." }
  ]
}

Each element of attempts is a Step Trace Object, ordered by attempt ascending. A step that did not retry returns a single-element list — not a 404. 404 is returned only when the step never ran at all.

Live-Tail Stream (SSE)

GET /flow-runs/{flowRunId}/trace/stream

Subscribe to a flow run's trace as Server-Sent Events. The connection always opens with a replay of every event that has happened so far (reconstructed from the persisted trace), then transitions to a live tail of new events as the worker emits them. After replay, the stream stays open until the run finishes or the client disconnects.

Content-Type: text/event-stream
Cache-Control: no-cache

Use this when:

  • The run was just started and you want a UI to react event-by-event.
  • The run started a moment ago and you want to catch up without polling.
  • The run is already finished — replay alone gives you the full event log in one stream.

Replay-then-tail, no gap. The server subscribes to the live channel before it begins replay, so events emitted during the replay window are not lost. The server dedupes its own emissions on (stepId, attempt, eventName) so you will never receive the same step+attempt+event-name combination twice on a single connection.

Connection lifecycle

  1. Server emits flow_started.
  2. For each step that has progressed: step_started, optionally step_input, then step_output or step_error, then step_completed. (Events are omitted when the data for them is not yet — or never — available, e.g. step_input is skipped under metadata_only capture.)
  3. If the flow run is already terminal at replay time, the server emits flow_completed and closes the stream.
  4. Otherwise the server holds the connection open and forwards events from the live channel as the run progresses, ending with flow_completed.

Idle keepalive: the server emits an SSE comment frame (: ping\n\n) every 15 seconds with no activity to keep proxies from closing the connection. Clients should ignore frames starting with :.

Treat any disconnect as terminal. A clean termination is flow_completed followed by the server closing the connection. If the connection drops without flow_completed (network, timeout, server crash), do not assume the run is still progressing — re-open the stream to receive the full replay and learn the run's current state. The replay is idempotent: any events already shown will arrive again with the same (stepId, attempt, eventName) and your client-side dedup keeps the UI consistent.

Authentication failures return an HTTP error (401 / 403) before the stream opens — they are not delivered as SSE events.

Event Vocabulary

Every event carries a string event: header and a JSON data: payload. The same vocabulary is used by Noukai's test runner — a UI written against these events handles both production runs and editor test runs.

flow_started

event: flow_started
data: { "flowRunId": "fr_01HX...", "flowId": "fl_01HX...", "startedAt": "2026-05-15T10:23:04.120Z" }
FieldTypeDescription
flowRunIdstringThe run being streamed.
flowIdstringThe flow this run belongs to.
startedAtstring | nullISO 8601 timestamp.

step_started

event: step_started
data: { "stepId": "summarize_essay", "attempt": 1, "startedAt": "2026-05-15T10:23:04.120Z", "blockName": "Summarize" }
FieldTypeDescription
stepIdstringStep identifier within the flow.
attemptinteger1-based retry counter.
startedAtstring | nullISO 8601 timestamp.
blockNamestring | nullHuman-readable name from the flow definition. May be omitted.

step_input

Emitted only when capture mode is full or redacted. Omitted under metadata_only and off.

event: step_input
data: { "stepId": "...", "attempt": 1, "inputContext": { "...": "..." }, "inputSizeBytes": 1284, "truncated": false }
FieldTypeDescription
stepIdstringStep identifier.
attemptintegerRetry counter.
inputContextobject | nullResolved input payload. May be null if the field was suppressed by redaction.
inputSizeBytesintegerPre-truncation byte size.
truncatedbooleantrue if the payload was truncated.

step_output

Emitted only when the step completed successfully and capture mode is full / redacted. Omitted under metadata_only and off.

event: step_output
data: { "stepId": "...", "attempt": 1, "outputContext": { "...": "..." }, "outputSizeBytes": 412, "truncated": false }
FieldTypeDescription
stepIdstringStep identifier.
attemptintegerRetry counter.
outputContextobject | nullThe block's output payload.
outputSizeBytesintegerPre-truncation byte size.
truncatedbooleantrue if the payload was truncated.

step_error

Emitted instead of step_output when the step failed.

event: step_error
data: { "stepId": "...", "attempt": 1, "errorContext": { "code": "MODEL_TIMEOUT", "message": "Upstream model timed out", "retryable": true } }
FieldTypeDescription
stepIdstringStep identifier.
attemptintegerRetry counter.
errorContextobjectStable fields: code (string), message (string), retryable (boolean). Additional fields may be present for debugging — treat them as opaque; the set is not part of the API contract.

step_completed

Final event for a step. Fires after step_output (on success), after step_error (on failure), or alone when the step was skipped.

event: step_completed
data: {
  "stepId": "summarize_essay",
  "attempt": 1,
  "status": "completed",
  "durationMs": 1840,
  "tokens": { "prompt": 412, "completion": 88, "total": 500 },
  "costUsd": "0.00041",
  "modelUsed": "openai/gpt-4o-mini"
}
FieldTypeDescription
stepIdstringStep identifier.
attemptintegerRetry counter.
statusstringcompleted, failed, or skipped.
durationMsintegerStep wall time. 0 for skipped steps.
tokensobject | null{ prompt, completion, total }. null for non-LLM blocks and skipped steps.
costUsdstring | nullDecimal string (see precision note in Step Trace Object). null when not metered.
modelUsedstring | nullResolved model slug. null for non-LLM blocks.

flow_completed

Last event in the stream. The server closes the connection after sending it.

event: flow_completed
data: { "flowRunId": "fr_01HX...", "status": "completed", "durationMs": 4821, "error": null }
FieldTypeDescription
flowRunIdstringThe run that just finished.
statusstringcompleted, failed, or cancelled.
durationMsintegerEnd-to-end wall time.
errorstring | nullStep ID where the run failed, when status: "failed". null for completed and cancelled.

Capture Modes

Each flow (or, by default, each organization) has a traceCaptureMode setting that controls how much detail is persisted per step. The setting also controls which SSE events are emitted.

ModeStep metadata (status, duration, tokens, cost)inputContext / outputContext returnedstep_input / step_output events
offRecorded as today (always)null; inputSizeBytes / outputSizeBytes also nullNot emitted
metadata_only (default)Recordednull; sizes populatedNot emitted
fullRecordedFull payload (truncated if oversized)Emitted
redactedRecordedRedacted payloadEmitted (already redacted)

Step metadata is always recorded regardless of mode — capture mode only controls payload persistence. The trace endpoints therefore continue to return run summaries and step lists even under off; only the payload fields and sizes change.

Size cap. full and redacted modes cap each payload at 256 KB. Oversized payloads are stored truncated with truncated: true and a __truncated__: true marker injected at the JSON root of inputContext / outputContext.

Retention. Trace payloads are retained for 30 days by default; metadata (status, duration, tokens, cost) is kept indefinitely.

Capture mode resolution order: flow.traceCaptureMode → organization.defaultTraceCaptureMode → "metadata_only". Switching modes only affects runs that start after the change.

Per-Attempt Semantics

A step that retries produces one flow_step_runs record per attempt, and one trace record per attempt.

  • The whole-run trace returns the latest attempt of every step.
  • The single-step trace defaults to the latest attempt and lets you opt in to all attempts (?attempt=all) or a specific one (?attempt=2).
  • The SSE live-tail stream emits a full event sequence per attempt. UI state machines should be keyed on (stepId, attempt) so an in-flight attempt and a completed earlier attempt of the same step don't overwrite each other in your local store. (This is the same key the server uses internally for its own dedup, plus the event name.)

Error Responses

StatusDescription
401Missing or invalid auth token.
403The caller has no access to the project that owns the flow run.
404Flow run, step, or specific attempt not found.
422Query parameter validation error — e.g. malformed attempt value.
500Internal error reconstructing the trace.

cURL Examples

List the 20 most recent runs of a flow:

curl -X GET "https://api.noukai.xyz/api/v1/flow-runs?flow_id=fl_01HX...&limit=20" \
  -H "Authorization: Bearer $NOUKAI_API_KEY"

Fetch the trace for a single run:

curl -X GET "https://api.noukai.xyz/api/v1/flow-runs/fr_01HX.../trace" \
  -H "Authorization: Bearer $NOUKAI_API_KEY"

Inspect every retry attempt for one step:

curl -X GET "https://api.noukai.xyz/api/v1/flow-runs/fr_01HX.../steps/summarize_essay/trace?attempt=all" \
  -H "Authorization: Bearer $NOUKAI_API_KEY"

Live-tail an in-progress run (curl prints each event as it arrives):

curl -N -X GET "https://api.noukai.xyz/api/v1/flow-runs/fr_01HX.../trace/stream" \
  -H "Authorization: Bearer $NOUKAI_API_KEY"

Minimal Node SSE consumer:

const res = await fetch(
  `https://api.noukai.xyz/api/v1/flow-runs/${flowRunId}/trace/stream`,
  { headers: { Authorization: `Bearer ${process.env.NOUKAI_API_KEY}` } },
);
 
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
 
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
 
  // Split on the SSE frame terminator
  let i: number;
  while ((i = buf.indexOf("\n\n")) !== -1) {
    const frame = buf.slice(0, i);
    buf = buf.slice(i + 2);
    if (frame.startsWith(":")) continue; // keepalive ping
    const event = /^event: (.+)$/m.exec(frame)?.[1];
    const data  = /^data: (.+)$/m.exec(frame)?.[1];
    if (event && data) handle(event, JSON.parse(data));
  }
}