Skip to content
NimbusNexus

Pipelines

A pipeline is a JSON document, and this page is its language. Webhooks and Request Bins run the same language on the same engine, so what you learn here holds there too, within each product's own header rules. To try any of this with no account, use the playground.

The document

This pipeline keeps paid invoices of $1,000 or more, removes the customer's email and any phone number, adds two fields and a header, and says a bearer token goes on the request:

JSON
{
  "version": 1,
  "steps": [
    { "filter": { "type": "invoice.paid", "data": { "amount": { "$gte": 100000 } } }, "note": "Paid invoices of $1,000 or more" },
    { "drop": ["data.customer.email", "**.phone"] },
    { "set": { "event_type": "{{ type }}", "amount_display": "{{ data.amount | money(data.currency) }}" } },
    { "headers": { "set": { "X-Event-Type": "{{ type }}" } } },
    { "auth": { "type": "bearer", "secret": "sec_…" } }
  ]
}

version is the document's format, always 1; it is not the pipeline's version. The steps run in the order written. Each is an object with one step key and, optionally, a note of up to 200 characters, which the console shows on the step and a drop reports. A filter can also take an action.

A run sees one request: a body, any JSON value; headers; and read-only meta. In the run API all three are whatever you send. Each step sees the body as the one before left it. Steps are numbered from 0 in the API and in error text, and from 1 in the console's editor.

StepIn the consoleWhat it does
filterFilterKeeps the request only if it matches, or, with "action": "drop", drops it if it does.
dropRemove fieldsRemoves fields from the body.
setSet fieldsAdds or overwrites fields.
mapReshape the bodyReplaces the body with one you build.
headersHeadersSets, removes or renames headers.
authCredentialSays that a bearer token, basic auth or an API key goes on the request, and names the slot it comes from.

filter

Keeps the request only if it matches; otherwise the run answers dropped. With "action": "drop" it works the other way round: what matches is dropped.

JSON
{ "filter": { "data": { "status": "paid", "amount": { "$gte": 100000 } } }, "note": "Paid, $1,000 or more" }
  • Every key is a condition, and all of them must hold. A nested object tests by shape, as data does here.
  • A plain value matches by JSON equality: 1 equals 1.0.
  • An array matches when it equals the value or when one of its elements matches: {"data": {"tags": "urgent"}} holds for ["billing", "urgent"], and {"data": {"items": {"sku": "A-1", "qty": 2}}} needs one item with both.
  • A field the request doesn't have matches only {"$exists": false} and the negations, such as $neq.
  • The operators are $eq, $neq, $gt, $gte, $lt, $lte (numbers with numbers, text with text; ISO timestamps sort by time), $in, $nin, $exists, $startsWith, $endsWith, $contains (text, or an array holding the value), and $and, $or and $not to combine filters.
  • $headers, $meta and $body test that part of the request: {"$headers": {"x-github-event": "push"}}.

drop

Removes fields from the body: a list of 1 to 50 paths. * matches any one level and ** any depth, so **.email removes every email field, wherever it is. A path that matches nothing is not an error.

JSON
{ "drop": ["data.customer.email", "data.cards[*].number", "**.phone"] }

set

Adds or overwrites fields: 1 to 50 pairs of a path and a value. It creates the objects on the way, and every value reads the body as the step found it. A path that runs into a value that isn't an object fails the run with set_conflict.

JSON
{ "set": { "event_type": "{{ type }}", "data.total_cents": { "$ref": "data.amount" } } }

map

Replaces the body with a value you write, usually an object.

JSON
{ "map": {
    "invoice_id": "{{ data.id }}",
    "amount": { "$ref": "data.amount" },
    "amount_display": "{{ data.amount | money(data.currency) }}",
    "event": "{{ type }}"
} }

On an invoice.paid request whose data is {"id": "in_42", "amount": 150000, "currency": "usd"}, the body becomes:

JSON
{ "invoice_id": "in_42", "amount": 150000, "amount_display": "1,500.00 USD", "event": "invoice.paid" }

headers

Sets headers with set, removes them with remove and renames them with rename, in the order remove, rename, set. Names match whatever their case, and a name that is set replaces every spelling of it.

JSON
{ "headers": { "set": { "X-Event-Type": "{{ type }}", "Content-Type": "application/json" } } }

A headers step can't set a header the connection owns, such as Host or Content-Length, or Set-Cookie. It can't set Authorization, Cookie or X-Api-Key either, because the value would sit in the document, which every read returns: a credential goes in an auth step. A product that runs the pipeline by reference also holds it to that product's own header rules.

auth

Says that a credential goes on the request, and names the slot it comes from:

typeAdds
bearerAuthorization: Bearer and the credential.
basicAuthorization: Basic, from a username and the credential as the password.
api_key, "in": "header"The credential in the header called name, after an optional prefix such as "Token ".
api_key, "in": "query"The credential as the query parameter name, on the destination's URL.
JSON
{ "auth": { "type": "api_key", "in": "header", "name": "X-Api-Key", "secret": "sec_…" } }

secret is the id of a slot the workspace holds. It is never a value: a value pasted there is refused with invalid_secret, and the refusal doesn't quote it back. The product that runs the pipeline by reference maps the slot to its own secret (Credentials). Test and the run API show the header or parameter with the value masked.

Auth steps come after every other step, so no step can read, copy or test a credential.

Paths and values

A path names a field of the body:

PathNames
data.customer.emailA field.
data.items[0].skuAn element of a list.
data['first name']A key with a space, a dot or a bracket in it.
data.items[*].skuEvery element; in a value, the list of them.
**.emailemail at any depth; in a drop only.
$meta.source, $headers.x-name, $body.dataThe request's meta, one of its headers, or its body; in a value only.

set takes plain paths, drop takes * and **, and a value reads with * but not **.

A value in set, map or a header is JSON, copied as written, except for two things:

  • {{ path }} in a string inserts a value as text. A missing value or null inserts nothing, and an object or a list goes in as JSON. {{ 'text' }} and {{ 42 }} are literals, so a literal {{ is written {{ '{{' }}.
  • {"$ref": "path"} inserts the value itself and keeps its type: a number stays a number, a list a list. Add "default" for when the path is missing.

An expression can pass its value through filters, left to right: {{ data.amount | money(data.currency) }} gives 1,500.00 USD for an amount of 150000 in usd.

FilterGives
upper, lowerThe text in upper or lower case.
default(x)x when the value is missing, null or empty.
truncate(n)At most n characters, then ….
join(sep)A list as text, joined with ", " unless you give sep.
lengthThe number of elements, keys or characters.
json, json(indent)The value as compact JSON, or indented by 1 to 8 spaces.
number(digits)A number with that many decimals.
money(currency)An amount in minor units, formatted; zero- and three-decimal currencies included.
date(format)An ISO 8601 time or epoch seconds, in UTC, with %Y, %m, %d, %H, %M, %S and the like.
escape(target)Text that can't format a chat message: slack (the default), discord or markdown.
code(target, max, language)Text as a code block in slack, discord or markdown markup, which the text can't end early, fitted to max characters.

There are no loops, conditions or sums: a condition is a filter step, and a list becomes text through [*] and join.

Templates

The gallery holds ready-made pipelines to start from. In the console they sit under Start from a template. Through the API, GET /v1/pipelines/templates returns each one with its pipeline. Saving one makes a pipeline that is your own: change it like any other, and nothing refers back to the template.

idTemplateWhat it sends
slackSlack messageFor a Slack incoming webhook: the event's type as a header, its id and time, and its data as a code block.
discordDiscord messageFor a Discord channel webhook: an embed with the event's type, its data as a code block, and its id and time. It mentions no one.
teamsMicrosoft Teams cardFor a Teams workflow webhook: an Adaptive Card with the event's type, its id and time, and its data in monospace.
keep_fieldsKeep only these fieldsThe event's id, type, created_at and version, and only the fields of data that its map lists, starting with data.id. A listed field the event doesn't have is sent as null.
drop_personalDrop personal dataThe event without email, phone, first_name, last_name, address, billing_address, shipping_address, date_of_birth and ip_address, at any depth. A plain name stays, since it often names a plan or a product.

Each chat template is a map step and a headers step that sets Content-Type: application/json, and it works on any event. A missing type reads "Event" and a missing id or time "—", the data is cut to fit the app's limits, and nothing in the event can format the message, mention anyone or end the code block early.

Limits

A pipeline has no loops, no clock and no network, and fixed caps bound each run. Past one, the run answers failed with the reason, and a document past one is refused when it is saved.

WhatLimitRefused as
A document, as compact JSON64 KiBtoo_large
Steps20too_many_steps
Paths in a drop, fields in a set, names in each part of a headers step50invalid_value
A step's settings, as compact JSON16 KiBtoo_large
A step's note200 charactersinvalid_value
Nesting, in a step and in a body32 levelstoo_deep, input_too_deep
The body a step reads256 KiBinput_too_large
The body a run answers with256 KiBoutput_too_large
One value a step renders64 KiBvalue_too_large
Work in one run100,000 values read or copied, text counting one per 128 characterstoo_many_nodes
Headers after each step50, or as many as the request carried if moretoo_many_headers
One header value8 KiBheader_too_large in a run, for a value with an expression; invalid_header when saved, for one without

The playground has smaller caps: a 16 KiB document, a 32 KiB body, 16 KiB for one value a step renders, 20 headers, 1 KiB for one header value and 10,000 values read or copied in a run.

What's next