Typed errors
Branch on error class, not on.message. Every error carries a stable .code, the run’s .traceId (when applicable), and a server-adjudicated .retryable flag.
The four failure layers
Read this rule carefully. If a Flow runs but fails mid-execution, the SDK does not throw — it returns
{ status: "failed", traceId, outputs: <partial>, failure: {...}, notices: [...] }. Silent try/catch that swallows only exceptions will miss these. Always check .status on the return value of flows.run.
Why the split
Anything that means “the request didn’t reach the graph” throws, so your caller code fails loud and early. Anything that means “the graph ran but the answer isn’t clean” comes back as a result, so you get:- the
traceIdfor the trace UI and forfeedback/signals.report, - whatever partial
outputswere produced, - a machine-readable
failure.codefor retry / degrade decisions, - constraint
notices[]that shaped the run (surface these in your admin UI, not to end users).
Logging
Structured and redaction-first by default — tokens, secrets,onBehalfOf values, and request/response bodies do not land in logs unless you turn debug: true on.
attempt, the failure code, and the decision. No silent retries.
Recommended:
- Route
loggerat Nora’s stream (pino / winston / bunyan) so log level and destination match the rest of your service. - Wire
onLoginto your metrics — retry rate, circuit-open rate, and provider-fallback frequency are the leading indicators when something upstream is degrading. - Keep
debugoff in production. Turn it on per-request via a scoped clone when reproducing an incident.