Code Steps: write any notification logic in TypeScript, for any channel
Every notification step - email, SMS, push, chat, in-app - can now be managed as TypeScript in your codebase. One CLI command to deploy, dashboard controls for your team.

The problem with notification content isn't specific to email.
Every channel - email, SMS, push, chat, in-app - has the same fundamental issue: the content lives in a dashboard, outside of version control, disconnected from your codebase.
You can't PR a subject line change. You can't run a notification template through CI. You can't fetch data from your own API at send time or apply conditional logic based on what a user actually did.
Template engines give you variables. Your application logic gives you everything else. Notification platforms have historically forced you to choose one.
Code Steps closes that gap. Every channel step in a Novu Workflow can now be managed as TypeScript in your codebase and deployed as a serverless function.
Novu calls your code at send time with the subscriber data, trigger payload and any dashboard-defined controls. Your code returns the output.
Why template engines break down
Every major notification platform ships with a template engine. Handlebars, Liquid, Mustache - the details differ but the model is the same: you write a template with {{variable}} placeholders, pass in data at send time, and the engine interpolates.
This works for simple cases. It falls apart the moment your email needs to do anything real.
No logic. Template engines can't branch on computed values, call external APIs, or derive content from multiple data sources. Every dynamic section has to be pre-computed and stuffed into the payload before triggering the workflow - which means your application layer ends up owning content decisions that belong in the template.
No type safety. There's no contract between your trigger call and your template. Pass the wrong key, spell a variable wrong, change your data model - the template silently renders empty strings or breaks at send time, not at compile time.
No reuse. Email design systems - shared headers, footers, button components, color tokens - don't exist in template engines. Every template is its own island. With React Email, you build a component library and compose it across templates.
No local developer experience. Dashboard template editors have no concept of hot reload, local preview with real data, or integration with your IDE. React Email has all of this.
React Email solves all of this. The only problem is that most notification platforms couldn't use it natively - until now.
How Code Steps works
Each step in a Novu Workflow now has two modes: Editor (visual, managed in the dashboard) and Custom Code (TypeScript, managed in your codebase). You choose per-step - a single workflow can mix both.
You write a step handler file using the channel-specific builder from @novu/framework/step-resolver:
Switch any step to Custom Code mode and the dashboard shows a pre-filled CLI command. Copy it and run it - the CLI scaffolds your handler file and deploys it as a serverless function on the Novu Cloud infrastructure.

Getting started
Switch any email step to Custom Code mode. Copy the pre-filled CLI command from the step editor and add the --template flag:
The CLI scaffolds the full handler file at novu/onboarding/welcome-email.step.tsx. Edit it, then re-run npx novu step publish to deploy.
When ready for production, use the Publish changes button in the Novu dashboard - the CLI publishes to dev only.
Dynamic content at send time
Because your handler is a real TypeScript function, it can call your own APIs at send time:
recentItems doesn't need to exist in the trigger payload - it's fetched at send time, per subscriber, from your own API.
This is the gap template engines have never closed.
The team collaboration problem
Define a controlSchema and the Novu dashboard renders editable fields for each value. Developers set the structure and defaults - marketing edits copy without touching code:

Skipping steps based on runtime logic
Use the skip function to conditionally skip a step at send time:
When skip returns true, the step is treated as executed but skipped. The decision is per-send, per-subscriber, based on real runtime data.
The deployment model
npx novu step publish bundles all handler files in your novu/ directory into a single Cloudflare Worker and deploys it. At send time, Novu calls your worker, validates the output, and passes it to your provider.
- Bundle size limit: 10 MB. Use
--workflowand--stepflags to publish specific handlers if needed. - CLI publishes to non-production only. Use the Publish changes button in the Novu dashboard to promote to production.
- Re-publishing is idempotent. No content change = no deploy.
Every channel, same model
Channel | Builder | Required output
Email | step.email() | subject, body
SMS | step.sms() | body
Push | step.push() | subject, body
Chat | step.chat() | body
In-App | step.inApp() | subject, body
Same CLI command, same deployment model, same controlSchema pattern across every channel. Mix Custom Code steps and Editor steps freely within the same Workflow.
Two sources of truth, one delivery system
Workflow configuration lives in Novu. Content logic lives in your application.
npx novu step publish is the bridge between them. The Publish changes button is the gate that controls what reaches production.
This is what programmable notification infrastructure means in practice - not a visual editor that lets you write code, not an SDK that requires you to define everything in code, but a system where each side owns what it does best.
Code Steps is available now. Switch any step to Custom Code mode in the Novu dashboard and copy the pre-filled command to connect your first handler.
For TypeScript types in your editor, install @novu/framework as a dev dependency - the CLI bundles its own copy for deployment, so this is optional but recommended.