docs/framework/studio.mdx
When building workflows with Novu Framework, you develop them locally in your code and preview them live through the Novu Dashboard. Running npx novu dev opens the Dashboard in Local mode - a local environment connected to your running application through a tunnel - where you can see and test the workflows running on your machine before you sync them to Novu Cloud.
To start local development, run the following command in your terminal:
npx novu@latest dev
This command:
/env/:environmentSlug/local/workflows).After your application is running and connected through the tunnel, the Local environment lists every workflow discovered from your Bridge Endpoint in real time. This is ideal for quick prototyping, debugging, styling, and adjusting your workflows before syncing them to Novu Cloud.
<Note> Nothing in the Local environment is stored in Novu Cloud. Workflows stream live from your machine, so the Development and Production environments keep showing their last synced state until you run a sync. </Note>You can quickly modify the Step Controls and workflow Payload to preview your workflow's different states. This is helpful to quickly debug how the email will behave in case of a missing control, or iterate more complex content structures. These edits live in your local session - they are not persisted to Novu Cloud.
The Local environment only reflects what is running on your machine, so it has no Publish flow. To make your workflows available in the Development or Production environments, you deploy your Bridge application and run the sync command against the deployed server - not against your local tunnel.
Novu Framework follows a GitOps model: the source of truth for your workflows lives in your Git repository as code. The recommended flow is to run the sync command from your CI/CD pipeline after each deployment:
npx novu@latest sync \
--bridge-url <YOUR_DEPLOYED_URL_WITH_BRIDGE_ENDPOINT> \
--secret-key <NOVU_SECRET_KEY> \
--api-url https://api.novu.co
See the Syncing guide for the full end-to-end flow and the available CI/CD integrations.
<Note> For quick experimentation you can also point a sync at your local tunnel URL, but the durable way to push changes to Development and Production is to deploy your code and sync against the deployed Bridge Endpoint from your CLI or CI. </Note>By default the Novu CLI will automatically create a tunnel URL connected to your local computer. This tunnel will proxy any workflow engine requests on our cloud to your local machine.
When npx novu@latest dev starts, it prints the full Bridge URL, for example:
š£ļø Tunnel ā https://your-tunnel.novu.co/api/novu
Use that value as bridgeUrl when triggering from your application (see below).
The Dashboard Local environment is virtual and scoped to your browser session - it only shows workflows running on the machine that started npx novu@latest dev. It is not a shared environment for the organization.
When you trigger a workflow from your application code, Novu normally executes against the Bridge URL synced to the Development (or Production) environment. If several engineers share the same organization, each can instead route those app-fired triggers to their own local tunnel by passing bridgeUrl on the trigger request:
const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY });
await novu.trigger({
workflowId: "workflow_identifier",
to: { subscriberId: "subscriber-id" },
payload: {
// your payload
},
// Full tunnel URL printed by npx novu@latest dev
bridgeUrl: process.env.NOVU_BRIDGE_URL,
});
</Tab>
<Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
novu.trigger(trigger_event_request_dto=novu_py.TriggerEventRequestDto(
workflow_id="workflow_identifier",
to={"subscriber_id": "subscriber-id"},
payload={},
# Full tunnel URL printed by `npx novu@latest dev`
bridge_url=os.getenv("NOVU_BRIDGE_URL"),
))
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
// Full tunnel URL printed by npx novu@latest dev
bridgeURL := os.Getenv("NOVU_BRIDGE_URL")
res, err := s.Trigger(context.Background(), components.TriggerEventRequestDto{ WorkflowID: "workflow_identifier", To: components.CreateToSubscriberPayloadDto(components.SubscriberPayloadDto{ SubscriberID: "subscriber-id", }), BridgeURL: &bridgeURL, }, nil) if err != nil { panic(err) } _ = res
</Tab>
<Tab title="PHP">
```php
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()
->setSecurity(getenv('NOVU_SECRET_KEY'))
->build();
$sdk->trigger(
triggerEventRequestDto: new Components\TriggerEventRequestDto(
workflowId: 'workflow_identifier',
to: new Components\SubscriberPayloadDto(subscriberId: 'subscriber-id'),
payload: [],
// Full tunnel URL printed by `npx novu@latest dev`
bridgeUrl: getenv('NOVU_BRIDGE_URL'),
),
);
var sdk = new NovuSDK(secretKey: Environment.GetEnvironmentVariable("NOVU_SECRET_KEY"));
await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() {
WorkflowId = "workflow_identifier",
To = To.CreateSubscriberPayloadDto(new SubscriberPayloadDto() {
SubscriberId = "subscriber-id",
}),
Payload = new Dictionary<string, object>() {},
// Full tunnel URL printed by npx novu@latest dev
BridgeUrl = Environment.GetEnvironmentVariable("NOVU_BRIDGE_URL"),
});
</Tab>
<Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.Map;
Novu novu = Novu.builder()
.secretKey(System.getenv("NOVU_SECRET_KEY"))
.build();
novu.trigger()
.body(TriggerEventRequestDto.builder()
.workflowId("workflow_identifier")
.to(To2.of(SubscriberPayloadDto.builder()
.subscriberId("subscriber-id")
.build()))
.payload(Map.of())
// Full tunnel URL printed by `npx novu@latest dev`
.bridgeUrl(System.getenv("NOVU_BRIDGE_URL"))
.build())
.call();
Recommended pattern for a team:
npx novu@latest dev and copies their tunnel Bridge URL.NOVU_BRIDGE_URL) to that URL - including the Bridge Endpoint path, typically /api/novu.bridgeUrl: process.env.NOVU_BRIDGE_URL so triggers hit that developer's machine.If you trigger via @novu/framework's workflow.trigger(), set NOVU_BRIDGE_ORIGIN to the tunnel origin only (without the path). The Framework SDK appends /api/novu and injects bridgeUrl automatically.
By default, the CLI will connect to the Novu Bridge Endpoint running on your local machine at http://localhost:4000/api/novu. If your server is running on a different port or the workflows are served from a different endpoint path you can use the following optional parameters to connect:
npx novu@latest dev --port <YOUR_SERVER_PORT> --route <YOUR_NOVU_ROUTE_PATH>
serve function. Defaults to /api/novu.The Novu CLI command npx novu@latest dev supports a number of flags:
| Flag | Long form usage example | Description | Default value |
|---|---|---|---|
| -p | --port <port> | Bridge application port | 4000 |
| -r | --route <route> | Bridge application route | /api/novu |
| -o | --origin <origin> | Bridge application origin | http://localhost |
| -d | --dashboard-url <url> | Novu Cloud dashboard URL | https://dashboard.novu.co |
| -t | --tunnel <url> | Self hosted tunnel url | null |
| -H | --headless | Run without opening the browser | false |
| --no-studio | Skip opening the Dashboard Local environment (still creates the tunnel and runs agent sync) | false |
Example: If bridge application is running on port 3002 and Novu account is in EU region.
npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.co
```bash
npx novu@latest dev -t http://custom-tunnel-url.ngrok.app
```
<Warning>
While the preview will work, you won't be able to test your notifications by triggering them from the Local environment UI.
</Warning>