A flow is a small program you draw instead of write. It's a graph of steps that read and write items in any of your lists, check who's asking, branch, loop over arrays, and respond. JSONPad runs it for you, so you don't need a server of your own for it.
A flow starts in one of three ways:
https://api.jsonpad.io/flows/<path>. Everything it writes happens together, or not at all, and it responds with whatever you choose.Flows are for the things your app can't safely do from the browser: writing to two lists at once, taking stock only if there's enough, dealing the top card of a deck the players can't see, or keeping a count up to date. A flow runs with your privileges, as a trusted command, and decides for itself who may do what.
This endpoint creates an order and takes the stock for it. If there isn't enough stock, or nobody is signed in, it refuses, and nothing is written. It's one of the templates on the Flows page in the dashboard.
Each box is a node, and runs after the nodes with edges into it. product reads an item, in_stock checks something is true, order and take_stock write, and out responds. A flow is stored as a JSON document, which is what the editor's JSON tab shows:
{version: 1name: "create-order"description: "Pair with a rule on orders like allow create: "server" in token.tags"entry: {type: "endpoint"method: "POST"path: "create-order"input: {type: "object"required: [0: "productId"1: "quantity"]properties: {productId: {type: "string"}quantity: {type: "integer"minimum: 1}}}}nodes: [0: {id: "signed_in"type: "require"when: "identity != null"status: 401message: "sign in first"}1: {id: "product"type: "get-item"list: "products"item: "input.productId"required: true}2: {id: "in_stock"type: "require"when: "steps.product.data.stock >= input.quantity"status: 409message: "not enough stock"}3: {id: "order"type: "create-item"list: "orders"identityId: "identity.id"data: "{ productId: steps.product.id, quantity: input.quantity, total: steps.product.data.price * input.qu..."}4: {id: "take_stock"type: "update-item"list: "products"item: "steps.product.id"merge: "{ stock: steps.product.data.stock - input.quantity }"ifVersion: "steps.product.version"}5: {id: "out"type: "respond"status: 201body: "{ orderId: steps.order.id, total: steps.order.data.total }"}]edges: [0: [0: "signed_in"1: "product"]1: [0: "product"1: "in_stock"]2: [0: "in_stock"1: "order"]3: [0: "order"1: "take_stock"]4: [0: "take_stock"1: "out"]]}Every value a node needs is an expression, written in the same language as write rules: steps.product.data.stock >= input.quantity. The flows reference lists every node and what it takes.
for-each node, whose body is a small graph of its own that runs once for each element of an array.condition node has two outputs, true and false, and only the edges from the one it chose are taken. A switch has an output for each case. Nodes on a branch that wasn't taken are skipped, and their steps.<id> is null.steps.<id> to the nodes after it: the item a get-item read, the array a find-items found, the value a compute worked out.require node stops the flow unless its expression is true, with the status and message you give it. So does anything else going wrong: an item that isn't there, a write your list's JSON schema refuses, running out of time.Expressions can read:
inputThe request body (or query parameters for a GET), the event for an event flow, or what you gave a run by hand.stepsThe results of the nodes before this one, by id.identityThe identity calling the flow (or making the change, for an event flow), or null.tokenThe API token calling the flow: its id and tags.nowThe time the run started. Every write in the run uses it.flowAbout the run: name, runId, eventId, depth and dryRun.A flow can also declare helpers: let and function declarations every expression in it can use.
1function isPlayer(game) = identity != null2 && identity.id in game.data.playerIds;34let maxQuantity = 10;
A flow reads and writes any of your lists, as you. It isn't limited by the permissions of the token that called it, it can read guarded values, and its writes skip your lists' write rules unless a node asks for them with applyRules. It sees activated lists and items only, and its writes do everything the item endpoints do: variables like $jsonpad-var:now, JSON schemas, indexes and aliases, your plan's limits, events, realtime updates and webhooks.
So a flow is a trusted command, and it does its own authorisation with require nodes: check identity, or token.tags, before it does anything that matters. The editor warns you about an endpoint flow that never looks at either.
That gives you the command pattern. Clients can't write to orders directly, because its write rules only let your server's token create orders:
1allow create: "server" in token.tags;
...so the only way a client can create an order is to call the create-order flow, which checks the stock and takes it in the same transaction.
An endpoint flow answers one method at one path: POST /flows/create-order, for example. Call it with one of your API tokens, and optionally an identity. The token needs the run permission on the flow, or run-with-identity with an identity. If the flow's entry has an input schema, input that doesn't match it is refused before the flow runs.
curl https://api.jsonpad.io/flows/create-order \
-H "Content-Type: application/json" \
-H "x-api-token: <YOUR TOKEN>" \
-H "x-identity-token: <IDENTITY TOKEN>" \
-d '{ "productId": "7b27d73e-2243-4954-99ed-a8c4d5d9b2d9", "quantity": 2 }'The response is whatever the flow's respond node chose: its status, body and headers. A flow without one responds 204 No Content. A flow that stops responds with the error status it stopped with, e.g. a require node's 409:
{name: "FLOW_FAILED"code: 23008message: "not enough stock"details: {code: "REQUIRE_FAILED"node: "in_stock"}}Every response has an x-flow-run header, the run's id, to find it in the flow's run log.
An endpoint flow with public turned on can also be called without a token, at /flows/public/<flow id>: a contact form, a sign-up list, a vote. Its require nodes are all that decides who may do what, so write them with care. Each run counts against your allowance, and one address can call a public flow at most 30 times a minute.
Open Flows in the dashboard, and create one, or start from a template. The editor has the graph on one side and, on the other:
Flows are part of your account's configuration, so they're managed by you, in the dashboard (or with schema sync), never with an ordinary API token.
A flow can have stored tests. Each gives the flow an input (and an identity, and the items it needs), runs it against an in-memory copy of those items, and checks what happened: the status and body, the error, the writes, or the items afterwards. The tests run every time the flow is saved or synced, and a flow whose tests fail isn't saved.
{
"identities": {
"alice": { "id": "alice-id", "name": "alice" }
},
"items": {
"products": {
"widget": { "data": { "price": 5, "stock": 3 } }
}
},
"tests": [
{
"name": "takes the stock and responds with the order",
"identity": "alice",
"input": { "productId": "widget", "quantity": 2 },
"expect": {
"status": 201,
"body": { "total": 10 },
"items": { "products": { "widget": { "stock": 1 } } }
}
},
{
"name": "refuses more than there is",
"identity": "alice",
"input": { "productId": "widget", "quantity": 4 },
"expect": { "status": 409, "error": "not enough stock", "writes": [] }
},
{
"name": "needs someone signed in",
"identity": null,
"input": { "productId": "widget", "quantity": 1 },
"expect": { "status": 401, "error": "sign in first" }
}
]
}Test items are keyed by an id you choose, like widget, and that's the id the flow sees. An expectation only has to mention what matters: bodies and items match if they have at least the fields given.
A flow is a JSON document, so it can live in your repository next to your schema sync document, which declares it along with the lists it uses. The command line tool checks and tests flows without the network, and syncs them with the rest of your schema.
{
"$schema": "https://jsonpad.io/schema/sync-v1.json",
"scope": "shop",
"lists": {
"products": { "name": "Products" },
"orders": {
"name": "Orders",
"rules": "allow create: \"server\" in token.tags;"
}
},
"flows": {
"create-order": {
"flowFile": "flows/create-order.flow.json",
"flowTestsFile": "flows/create-order.tests.json"
}
}
}Where the nodes sit in the editor isn't part of what's compared, so tidying a flow up in the dashboard doesn't make the file out of date. Syncing flows needs a token that is allowed everything, because a flow runs with your full privileges.
Each flow's page in the dashboard shows its last 100 runs: when, how it started, how it ended, how long it took and what it cost. Select a run to see which way each node went on the graph, and its input. The log never holds your items' data: only whether each node ran, and the values that were true or false.
A flow costs what the same API calls would have: calling it is one request, and each item it reads or writes is one more. Runs you start in the dashboard, and test runs, are free. Each plan allows a number of flows: 2 on Free, 10 on Indie, 50 on Pro and 250 on Scale.
These limits are the same on every plan:
100 nodesper flow, counting the nodes in a for-each's body once50 reads and writesper run, of which at most 25 writes50 elementsper for-each, each time it runs5 secondsfor an endpoint flow, and 30 for an event flowThe reference has the rest.
Flows don't call other services yet. To tell your own server about something a flow did, use an emit node, which sends a signed event to one of your webhooks once the run has committed.