docs/.mintlify/skills/design-workflow/references/step-conditions.md
A step condition decides whether a step runs or is skipped. Same semantics on both authoring surfaces; different syntax.
| Surface | Syntax |
|---|---|
| Dashboard (no-code) | JSON-Logic on step.condition — { "==": [...] } |
Framework (@novu/framework) | skip: () => boolean | Promise<boolean> callback |
Dashboard semantics: condition evaluates to
true⇒ step runs. Framework semantics:skipreturnstrue⇒ step is skipped. They're mirror images — invert the boolean when porting between surfaces.
Use only variables that are in scope for the workflow run.
Prefer reusing existing variables for consistency. Only introduce new
payload.*variables when truly needed — duplication makes templates and conditions harder to maintain.
| Namespace | Source | Contents |
|---|---|---|
workflow.* | system | Workflow metadata: workflowId, name, description, tags, severity |
subscriber.* | system | Recipient info: firstName, lastName, email, phone, avatar, locale, timezone, subscriberId, isOnline, lastOnlineAt, data |
payload.* | user-defined | Event data passed at trigger time (e.g. actionUrl, productName, orderNumber). Validated against payloadSchema when defined. |
steps.* | system | Step results: In-App seen / read, digest events / eventCount, HTTP response properties (only those declared in responseBodySchema) |
context.* | user-defined | Multi-tenant metadata passed at trigger time (e.g. tenant, region, app). See inbox-integration/SKILL.md for context-based isolation. |
Available subscriber.* properties:
subscriber.firstNamesubscriber.lastNamesubscriber.emailsubscriber.phonesubscriber.avatarsubscriber.localesubscriber.timezonesubscriber.subscriberIdsubscriber.isOnlinesubscriber.lastOnlineAtsubscriber.data (custom subscriber data; deeply addressable as subscriber.data.<key>)| Path | When available | Notes |
|---|---|---|
steps.<stepId>.seen | After an In-App step | Boolean — true once the user has seen the notification |
steps.<stepId>.read | After an In-App step | Boolean — true once the user has marked it read |
steps.<stepId>.events | After a digest step | Array of digested trigger events |
steps.<stepId>.eventCount | After a digest step | Length of events (convenience for templates) |
steps.<stepId>.<prop> | After an HTTP step | Only properties declared in responseBodySchema are addressable |
{ "==": [{ "var": "subscriber.isOnline" }, "false"] }
{ "==": [{ "var": "steps.<stepId>.read" }, "false"] }
{ "==": [{ "var": "steps.<stepId>.seen" }, "false"] }
{ "in": ["tag1,tag2", { "var": "workflow.tags" }] }
{ "==": [{ "var": "steps.<http-step-id>.status" }, "active"] }
The property must be declared in the HTTP step's
responseBodySchema. Undeclared properties are not addressable.
The same conditions in @novu/framework. Note that skip is the inverse of "run if true" — you return true to skip.
const inApp = await step.inApp("inbox", async () => ({ /* ... */ }));
await step.push("offline-push", async () => ({ title: "...", body: "..." }), {
skip: ({ subscriber }) => subscriber.isOnline === true,
});
const inApp = await step.inApp("inbox", async () => ({ /* ... */ }));
await step.delay("wait", async () => ({ unit: "hours", amount: 4 }));
await step.email("fallback", async () => ({ subject: "...", body: "..." }), {
skip: () => inApp.read === true,
});
const plan = await step.http("fetch-plan", async () => ({
method: "GET",
url: `https://api.example.com/users/${payload.userId}/plan`,
responseBodySchema: {
type: "object",
properties: { status: { type: "string" } },
required: ["status"],
} as const,
}));
await step.email("notify", async () => ({ /* ... */ }), {
skip: () => plan.status !== "active",
});
| Intent | Dashboard (JSON-Logic) | Framework (skip) |
|---|---|---|
| Run only when subscriber is offline | { "==": [{ "var": "subscriber.isOnline" }, "false"] } | skip: () => subscriber.isOnline === true |
| Run only when In-App not read | { "==": [{ "var": "steps.inbox.read" }, "false"] } | skip: () => inAppResult.read === true |
| Run only when In-App not seen | { "==": [{ "var": "steps.inbox.seen" }, "false"] } | skip: () => inAppResult.seen === true |
| Run only for workflows tagged "billing" | { "in": ["billing", { "var": "workflow.tags" }] } | skip: () => !tags.includes("billing") |
Run only when HTTP status == "active" | { "==": [{ "var": "steps.fetch.status" }, "active"] } | skip: () => fetchResult.status !== "active" |
true; Framework skip skips when true. They're opposites.responseBodySchema are addressable in steps.<http>.<prop>.subscriber.isOnline == true as a string — isOnline is a boolean. Use "false" (string) only in JSON-Logic; in Framework use the JS boolean false.channel-selection.md — uses these conditions for offline gatingworkflow-templates.md — every template's Step condition lines map to these snippetsframework-integration/references/workflow-and-steps.md — full Framework skip referencedashboard-workflows/references/step-conditions.md — Dashboard / Novu MCP authoring flow, including the merge / replace / remove intent rules