Router Node
The Router node inspects the event payload and sends it down one of several branches based on an ordered list of rules. Build conditional branching without code — payment events to one branch, subscription events to another, all from a single flow.
Overview
A Router evaluates an ordered list of rules against the incoming payload. Each rule is a field path, an operator and a comparison value, and each rule has its own output port on the canvas — what hangs from that port is where a matching event goes. The first rule that matches and has something connected wins: the payload goes down that branch, untouched. A rule that matches but has nothing wired to it is skipped, and the next rules are tried. If no rule matches, the event stops there — there is no default branch; put a catch-all rule last (an exists on a field every event has) if you want one.
A Router with no rules at all passes everything through its main output, so you can drop one into a flow and wire it up later.
On the canvas, Router nodes appear as blue rectangles with one handle per rule. They run as part of the pipeline, in order, like any other node.
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
| name | string | — | Friendly label for the router |
| rules | Rule[] | [] | Ordered list of routing rules (see below). Order matters: the first match with a wired port wins |
| inputNodes | { nodeType, nodeId }[] | [] | Upstream nodes that feed this one. nodeType is the node kind (webhook, scheduledWorkflow, filter, …) |
| outputNodes | { nodeType, nodeId, port }[] | [] | What hangs from each rule. port is the rule's output port, rule:<rule id>. With no rules, entries on the main port (or with no port) are the pass-through output |
| examplePayload | object | null | The example used by Run Test, saved on the node so it is there when you come back |
| isActive | boolean | true | Whether the router is enabled |
Rule object
| Field | Type | Description |
|---|---|---|
| id | string | Stable id of the rule. It names the rule's output port on the canvas (rule:<id>), so the edges survive a rename |
| label | string | Optional name shown on the handle and in the run log. Without it, the field path is shown |
| field | string | Where to look: a dot path into the payload (data.type), or a template — {{payload.data.type}}, or {{$("Other node").field}} to read another node's output |
| operator | string | Comparison operator (see operators table) |
| value | string | What to compare against. Can be a template too. Not needed for exists / not_exists |
Supported operators
The Router evaluates rules with the same engine as the Filter node, so every Filter operator works here, plus two that only make sense when picking a branch: in and not_in, which compare one field against a list you write in the rule (a Filter says the same thing with several rules in or mode). Operators marked “no value needed” ignore value.
| Operator | Description | Example value |
|---|---|---|
| eq | Exact match (string comparison) | payment_intent.succeeded |
| neq | Does not equal | draft |
| gt / gte / lt / lte | Numeric comparison | 1000 |
| contains / not_contains | String contains (or does not contain) the value | error |
| starts_with / not_starts_with | String prefix | ORD- |
| ends_with / not_ends_with | String suffix | |
| matches_regex | Matches a regular expression | ^[A-Z]{3}-\d+$ |
| is_empty / is_not_empty | Empty string, empty array or empty object | (no value needed) |
| exists / not_exists | Field is present and not null, or absent/null | (no value needed) |
| has_key | Object has the given key | customer |
| is_true / is_false | Boolean value | (no value needed) |
| array_contains / array_not_contains | Array includes (or not) the value | vip |
| array_empty / array_not_empty | Array has no items, or at least one | (no value needed) |
| array_length_eq / array_length_gt / array_length_lt | Array length compared to a number | 3 |
| date_eq / date_before / date_after | Date comparison (ISO dates or timestamps) | 2026-01-01 |
| in | Value is in a comma-separated list — Router only | active,pending,processing |
| not_in | Value is not in a comma-separated list — Router only | cancelled,refunded |
Create a Router
curl -X POST /api/routers \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"workspaceId": "<your-workspace-id>",
"name": "Payment Event Router",
"rules": [
{ "id": "succeeded", "label": "Succeeded", "field": "type", "operator": "eq", "value": "payment_intent.succeeded" },
{ "id": "failed", "label": "Failed", "field": "type", "operator": "eq", "value": "payment_intent.payment_failed" },
{ "id": "subs", "label": "Subscriptions", "field": "type", "operator": "contains", "value": "subscription" }
],
"inputNodes": [{ "nodeType": "webhook", "nodeId": "6654a1b2c3d4e5f6a7b8c9d0" }],
"outputNodes": [
{ "nodeType": "slackAction", "nodeId": "6654e1f2a3b4c5d6e7f8a9b0", "port": "rule:succeeded" },
{ "nodeType": "emailAction", "nodeId": "6654e1f2a3b4c5d6e7f8a9b1", "port": "rule:failed" },
{ "nodeType": "httpAction", "nodeId": "6654e1f2a3b4c5d6e7f8a9b2", "port": "rule:subs" }
],
"isActive": true
}'Wiring is usually done on the canvas — drag from a rule's handle to the node it should feed — but it is the same outputNodes entry either way: the target node and the rule's port in port.
Response
{
"id": "6654d4e5f6a7b8c9d0e1f2a3",
"name": "Payment Event Router",
"rules": [
{ "id": "succeeded", "label": "Succeeded", "field": "type", "operator": "eq", "value": "payment_intent.succeeded" },
{ "id": "failed", "label": "Failed", "field": "type", "operator": "eq", "value": "payment_intent.payment_failed" },
{ "id": "subs", "label": "Subscriptions", "field": "type", "operator": "contains", "value": "subscription" }
],
"inputNodes": [{ "nodeType": "webhook", "nodeId": "6654a1b2c3d4e5f6a7b8c9d0" }],
"outputNodes": [
{ "nodeType": "slackAction", "nodeId": "6654e1f2a3b4c5d6e7f8a9b0", "port": "rule:succeeded" },
{ "nodeType": "emailAction", "nodeId": "6654e1f2a3b4c5d6e7f8a9b1", "port": "rule:failed" },
{ "nodeType": "httpAction", "nodeId": "6654e1f2a3b4c5d6e7f8a9b2", "port": "rule:subs" }
],
"isActive": true,
"examplePayloadTemplate": null,
"examplePayload": null,
"createdAt": "2025-05-01T12:00:00.000Z",
"updatedAt": "2025-05-01T12:00:00.000Z"
}Update a Router
curl -X PATCH /api/routers/6654d4e5f6a7b8c9d0e1f2a3 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{ "id": "succeeded", "label": "Succeeded", "field": "type", "operator": "eq", "value": "payment_intent.succeeded" },
{ "id": "high", "label": "High value", "field": "data.object.amount", "operator": "gt", "value": "10000" }
]
}'rules replaces the entire array. Always include all rules you want to keep — omitted rules are removed, and with them the edges wired to their ports. Keep the same id on a rule you are only editing, or its branch comes loose.Canvas Integration
The Router uses canvas type router and renders in blue, with one output handle per rule, labelled with the rule's label (or its field). Drag from a handle to the node that branch should feed; that writes an outputNodes entry with port: "rule:<id>". The card shows how many rules there are and how many have a destination, and warns when none does — such a router drops every event that reaches it, silently.
Input and output
The Router does not change the payload. What comes in is what goes down the chosen branch — _meta included, so nodes that key off iterable and count keep working. What the Router decides is which branch, and that decision is what Run Test and the run log show.
Incoming event — matches the first rule
{
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_abc123",
"amount": 5000,
"currency": "usd"
}
}
}Result: the payload goes down the Succeeded branch, because type equals payment_intent.succeeded and that port has a node wired to it. The other two branches see nothing.
{
"matched": "Succeeded",
"targets": [{ "targetNodeType": "slackAction", "targetNodeId": "6654e1f2a3b4c5d6e7f8a9b0" }],
"rulesEvaluated": 3,
"passthrough": false
}Incoming event — no match
{
"type": "invoice.finalized",
"data": {
"object": {
"id": "inv_xyz789",
"total": 3000
}
}
}Result: no rule matches, so the event stops at the Router. Nothing downstream runs, and the step is recorded with matched: null. To catch everything else, add a last rule such as type exists and wire it to the fallback branch.
Use Cases
- Event type routing — Route Stripe events by type to dedicated processing services.
- Priority routing — Send high-value transactions (amount > $100) to a priority queue and low-value ones to standard processing.
- Regional routing — Route events by
data.regionto region-specific webhooks. - A/B testing — Use the
inoperator on a customer or region list to split traffic across different processing versions. - Error isolation — Route error events to a dedicated error-handling service while normal events go to the main pipeline.
API Reference
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/routers | List all routers |
| POST | /api/routers | Create a new router |
| GET | /api/routers/:id | Get a specific router |
| PATCH | /api/routers/:id | Update a router |
| DELETE | /api/routers/:id | Delete a router |
| POST | /api/routers/:id/test | Run the rules against a payload without dispatching anything: returns matched, targets, rulesEvaluated and passthrough |
payment_intent.succeeded before a broader contains: payment rule.