Skip to main content

Workflow & Step API Reference

Full reference for the workflow() 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

Returns: 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/skip callbacks.

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.
Webhook-style fan-out:
Constraints:
  • responseBodySchema is 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.custom with your own idempotency key.
  • The Liquid {{subscriber.*}} and {{payload.*}} variables are usable inside url, headers, and body values.

Step Options

controlSchema

Defines no-code controls editable in the Dashboard. Pass a Zod schema, JSON Schema (as const), or Class-Validator class.
Control values support LiquidJS templating:
  • {{subscriber.firstName}}
  • {{payload.userId}}
  • {{payload.invoiceDate | date: '%b %d, %y'}}
  • {{subscriber.firstName | capitalize | append: '!'}}

skip

Skip a step based on dynamic logic.
The function receives the resolved controls and returns boolean | Promise<boolean>.

providers (Per-Step Overrides)

Override the request sent to the underlying provider SDK.
The _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.
For Inbox HTML rendering, also use 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.custom so the result is persisted in durable context.

Type Inference

When payloadSchema and controlSchema are provided as Zod or JSON Schema (with as const), payload and controls are fully typed:
If you don’t supply a schema, 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, severity
  • subscriber.*subscriberId, firstName, lastName, email, phone, avatar, locale, timezone, isOnline, lastOnlineAt, data.*
  • payload.* — any field declared in payloadSchema
  • steps.<stepId>.* — In-App seen / read, digest events / eventCount, HTTP properties declared in responseBodySchema
  • context.* — 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.