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

FieldTypeDefaultDescription
namestring—Friendly label for the router
rulesRule[][]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
examplePayloadobjectnullThe example used by Run Test, saved on the node so it is there when you come back
isActivebooleantrueWhether the router is enabled

Rule object

FieldTypeDescription
idstringStable id of the rule. It names the rule's output port on the canvas (rule:<id>), so the edges survive a rename
labelstringOptional name shown on the handle and in the run log. Without it, the field path is shown
fieldstringWhere 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
operatorstringComparison operator (see operators table)
valuestringWhat 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.

OperatorDescriptionExample value
eqExact match (string comparison)payment_intent.succeeded
neqDoes not equaldraft
gt / gte / lt / lteNumeric comparison1000
contains / not_containsString contains (or does not contain) the valueerror
starts_with / not_starts_withString prefixORD-
ends_with / not_ends_withString suffix.pdf
matches_regexMatches a regular expression^[A-Z]{3}-\d+$
is_empty / is_not_emptyEmpty string, empty array or empty object(no value needed)
exists / not_existsField is present and not null, or absent/null(no value needed)
has_keyObject has the given keycustomer
is_true / is_falseBoolean value(no value needed)
array_contains / array_not_containsArray includes (or not) the valuevip
array_empty / array_not_emptyArray has no items, or at least one(no value needed)
array_length_eq / array_length_gt / array_length_ltArray length compared to a number3
date_eq / date_before / date_afterDate comparison (ISO dates or timestamps)2026-01-01
inValue is in a comma-separated list — Router onlyactive,pending,processing
not_inValue is not in a comma-separated list — Router onlycancelled,refunded

Create a Router

bashCreate 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

json201 Created
{
  "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

bashUpdate 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" }
    ]
  }'
⚠Updating 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

jsonPayload
{
  "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.

jsonWhat Run Test reports
{
  "matched": "Succeeded",
  "targets": [{ "targetNodeType": "slackAction", "targetNodeId": "6654e1f2a3b4c5d6e7f8a9b0" }],
  "rulesEvaluated": 3,
  "passthrough": false
}

Incoming event — no match

jsonPayload
{
  "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.

ℹA rule that matches but has nothing connected to its port is skipped as if it had not matched, and the next rules are tried. That way a half-configured rule never swallows events; the card on the canvas tells you how many rules are wired.

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.region to region-specific webhooks.
  • A/B testing — Use the in operator 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

MethodEndpointDescription
GET/api/routersList all routers
POST/api/routersCreate a new router
GET/api/routers/:idGet a specific router
PATCH/api/routers/:idUpdate a router
DELETE/api/routers/:idDelete a router
POST/api/routers/:id/testRun the rules against a payload without dispatching anything: returns matched, targets, rulesEvaluated and passthrough
✦Routes are evaluated in order — place more specific rules first. For example, check for payment_intent.succeeded before a broader contains: payment rule.