Workflow & Step API Reference
Full reference for theworkflow() and step.* APIs in @novu/framework.
workflow(id, handler, options?)
workflowId: string
Unique identifier within your environment. Used as the trigger key in novu.trigger({ workflowId }). Convention: kebab-case (weekly-digest, password-reset).
handler: ({ step, payload, subscriber }) => Promise<void>
The body of the workflow. Receives:
options: WorkflowOptions
severity vs critical vs readOnly
Three distinct dials — pick deliberately. See design-workflow/references/severity-and-critical.md for the full matrix.
critical: true is a stronger guarantee than readOnly: true. Use critical when you need to force delivery; use readOnly only when you want to hide the toggle.
Workflow Preferences
Channel Steps
All channel steps share the same shape:step.email
void.
step.sms
step.push
step.chat
For Slack
blocks, Discord embeds, etc., use providers overrides — see below.
step.inApp
Use the result to drive
skip on subsequent steps (e.g. don’t email if already read).
Action Steps
step.delay
Pause workflow execution.
Returns:
{ duration: number } (in milliseconds).
If a delay step fails, the workflow stops — it does not proceed to the next step.
step.digest
Aggregate triggers over a time window or cron schedule.
Each
DigestEvent is { id: string, time: Date, payload: object }.
Constraints:
- One digest step per workflow. For two-stage digests, trigger a second workflow from
step.custom. - Digest content captured at trigger time — editing the workflow doesn’t affect events already in flight.
- Digest results are not available in step controls — only inside subsequent step
resolver/providers/skipcallbacks.
step.custom
Run arbitrary code and persist its output.
The return value must be JSON-serializable. The result is persisted in durable execution context and re-used on retries.
step.http
Call an external HTTP endpoint as part of the workflow — for fetching just-in-time data, posting to a webhook, or fanning out to a downstream service.
Constraints:
responseBodySchemais required when subsequent steps reference response data. Only properties declared in the schema are available as{{ steps.<http-step-id>.<property> }}(Dashboard) or as typed fields on the returned object (Framework).- The HTTP step participates in retries. Treat it as a side effect — if you need exactly-once external calls, prefer
step.customwith your own idempotency key. - The Liquid
{{subscriber.*}}and{{payload.*}}variables are usable insideurl,headers, andbodyvalues.
Step Options
controlSchema
Defines no-code controls editable in the Dashboard. Pass a Zod schema, JSON Schema (as const), or Class-Validator class.
{{subscriber.firstName}}{{payload.userId}}{{payload.invoiceDate | date: '%b %d, %y'}}{{subscriber.firstName | capitalize | append: '!'}}
skip
Skip a step based on dynamic logic.
boolean | Promise<boolean>.
providers (Per-Step Overrides)
Override the request sent to the underlying provider SDK.
_passthrough block deep-merges into the final provider request — typed provider keys take precedence over _passthrough.
disableOutputSanitization
Allow raw HTML / unescaped characters in step output.
dangerouslySetInnerHTML in renderBody / renderSubject (see inbox-integration).
Conditional Patterns
Send email only if in-app wasn’t seen
Skip delay for premium users
Branch on a fetched value
Failure & Retries
- If a delay or digest step fails, the workflow stops — subsequent steps do not run.
- If a channel step fails delivery, retries depend on provider config and Novu’s retry policy.
- Workflow handlers may be re-invoked on retry. Keep them deterministic — push side effects into
step.customso the result is persisted in durable context.
Type Inference
WhenpayloadSchema and controlSchema are provided as Zod or JSON Schema (with as const), payload and controls are fully typed:
payload and controls are unknown.
Appendix: Step Conditions (Dashboard JSON-Logic ↔ Framework skip)
Dashboard authors gate a step with JSON-Logic on step.condition. Framework authors pass a skip: () => boolean callback. The semantics are inverse — Dashboard runs when the condition is true, Framework skip skips when the callback returns true.
Variables you can reference in either surface (full breakdown in
design-workflow/references/step-conditions.md):
workflow.*—workflowId,name,description,tags,severitysubscriber.*—subscriberId,firstName,lastName,email,phone,avatar,locale,timezone,isOnline,lastOnlineAt,data.*payload.*— any field declared inpayloadSchemasteps.<stepId>.*— In-Appseen/read, digestevents/eventCount, HTTP properties declared inresponseBodySchemacontext.*— multi-tenant metadata passed at trigger time (tenant, region, app)
See design-workflow/references/step-conditions.md for the full list of canonical conditions and the design reasoning.