Busabase

Workflows

Automate a repeatable process with steps that call out, wait, re-check your data, and ask a person to decide.

A Workflow runs a process you repeat: a new lead comes in, Busabase scores it, tells your CRM, waits a day, checks the lead again, and asks your sales lead to approve the handover. You draw the steps once, and each run goes through them on the server. Nobody has to keep a tab open.

A workflow on the canvas with its Runs tab open, showing which steps of the selected run are done.

Build a workflow

Create it

Press + in the sidebar and choose Workflow. If it isn't listed, open More types.

Add steps and connect them

Add steps with + in the workflow's toolbar, then drag from one step to the next. Select a step to set it up under Configure.

Save

Press Save. Runs always use the last saved version, so starting a run is blocked while you have unsaved changes.

What each step does

StepWhat it doesConnections out
TriggerWhere a run starts. It reads the record the run is about, if there is one.One
FunctionRuns JavaScript written in the step, see Write code in a step. It can also run a saved webhook rule whose action is Run function. Its return value stays on the step.success
WebhookSends one HTTP request with the run's data as a JSON body (GET sends no body), with any Headers you add. The response becomes the step's output.success
ConditionChecks an expression and takes one of two paths.true, false
WaitPauses the run for minutes, hours, or days. The pause survives restarts and deployments.elapsed
ActionReads the record again, or updates, creates, or deletes one. See Change records.success
ApprovalPauses until someone approves or rejects.approved, rejected
EndFinishes the run with an outcome.None

A connection's Branch outcome decides when it is followed. A run never visits the same step twice, and it stops with an error after 64 steps.

Use earlier results in a condition

A condition can read the step right before it as input, and any earlier step through its reference. Select a step and Use this step in formulas as shows what to write, with a button to copy it:

input.status == 200 && steps.score.score >= 80

&& and || stop as soon as the answer is known, and a value that is missing counts as null, so input.body && input.body.ok is safe. Use brackets for names that are not plain words: fields["follow-up"], items[0].

Does a Wait see the latest data?

Only if you ask it to. Every finished step keeps a snapshot of what it saw, so steps.trigger still shows the record as it was when the run began. To work with current values, put an Action step set to Read the record again after the Wait. It reloads the record, and the next step sees it as input.record.

Change records

An Action step can change your data. Choose what it does from the list:

ActionWhat it doesSettings
Update a recordSets fields on a record.Record (optional), Fields to set
Create a recordAdds a record to a Base. Later steps get its id as createdRecordId on this step.Base, Fields to set
Delete a recordMoves a record to its Base's trash, where it can be restored.Record (optional)
Find recordsLists a Base's records, or the ones a View shows. See Act on many records.Base, View (optional), At most, Run the next steps once for each record

An Action step set to Create a record, with its Base, the fields to set, and how to refer to the step in formulas.

Record is the run's own record when left empty. It can also be a record ID, or a formula such as =steps.add.createdRecordId to change the record an earlier step created.

Fields to set is a JSON object:

{ "status": "qualified", "score": "=steps.score.score" }
  • Use a field's name or slug as the key. Plain values are stored as written.
  • A text value that starts with = is a formula over earlier steps. Start with == to store text that itself begins with =.
  • A formula that finds nothing fails the step, so a typo never blanks a field or changes the wrong record.

What else to expect:

  • The step acts as the person the run acts as, with their access to that Base. If they can edit it, the change applies right away. If they can only propose changes, it goes to the Inbox for review, nothing changes yet, and the step fails and names the change request. With On error set to Continue, the run goes on.
  • A change a step applies starts no workflow, including workflows listening to another Base. So a workflow that updates the record it listens to cannot loop, and workflows cannot set each other off. Changes a person makes, or approves later, start workflows as usual.
  • If a run deletes its own record, later steps carry on without one, except Read the record again, which fails.

Act on many records

A scheduled run is not about any record. To work through records, add an Action step set to Find records:

  1. Pick the Base, and a View to take only the records it shows, in its order, such as an "Overdue" view. All records takes every record, newest first.
  2. Set At most: up to 200 records. Empty means 50.
  3. Choose what the steps after it get:
    • The list, once. Later steps read steps.<id>.records, count, and total. A Function step can loop over them, for example to post one summary. When more records match than At most, the list stops there and truncated is true.
    • Each record, in its own run. Turn on Run the next steps once for each record. The run ends at this step, and every record found gets a run that carries on from the next step, about that record: Update a record with an empty Record changes it, a Function step sees it as input.record, and formulas read it as steps.<id>.record.fields.<field>.

With each record in its own run:

  • If more records match than At most, no run starts and the step fails with the count, so a View that grows by mistake cannot start hundreds of runs. Narrow the View, or raise the limit.
  • The runs show as Per record under Runs. They take turns under Concurrency, and the scheduler starts them within a minute or so. Cancel N queued runs stops the ones that have not started.
  • A run started this way does not repeat again: a second Find records set to run once for each record fails. While 200 runs of the workflow are still waiting, it starts no more and fails.

Write code in a step

A Function step runs the JavaScript in its Code box on the server. Use it for anything the other steps don't do: call an AI agent's API, post to a chat tool, reshape data, or reply to whoever sent a request. Ask an agent to write it for you.

  • input has the run's data: input.payload, input.steps.<id> for earlier steps, and input.record when the run has one.
  • await fetch(url, options) calls public URLs and answers like the standard fetch: res.ok, res.status, await res.json(), await res.text(), res.headers.get(name). Private and internal addresses are refused.
  • What you return is the step's output. Later steps read it as steps.<id>.<field>, and conditions can branch on it.
  • Limits: the workflow's Timeout, 10 fetch calls per step, and answers are cut at 32,000 characters. There is no require, file access, or environment.
  • Anyone who can open the workflow can read the code, so use keys made for this purpose that you can revoke.

Run heavier code in a sandbox

Code in a step runs inside the app: fast, but JavaScript only, with no packages and the workflow's Timeout. When a step needs more, such as an npm or pip package, Node's crypto, Python, or a few minutes, set Run on to A sandbox machine:

  1. Choose the Language: JavaScript (Node.js) or Python.
  2. List the Packages to install first, separated by spaces. Versions are allowed (lodash@4, requests==2.32.3).
  3. Write the code as before: input has the run's data and what you return is the step's output. In JavaScript load packages with require("name"); in Python import them as usual.
// JavaScript in a sandbox: Node's crypto signs the request, no package needed
const crypto = require("node:crypto");
const body = JSON.stringify({ text: input.payload.request?.body?.text ?? "" });
const signature = crypto.createHmac("sha256", "<shared secret>").update(body).digest("hex");
const res = await fetch("https://partner.example.com/hook", {
  method: "POST",
  headers: { "content-type": "application/json", "x-signature": signature },
  body,
});
return { status: res.status };
# Python in a sandbox, with Packages set to: requests
import requests

res = requests.get("https://api.github.com/repos/busabase/busabase", timeout=10)
return {"stars": res.json()["stargazers_count"]}

What to expect:

  • When the run reaches the step, a fresh machine starts, installs the packages (up to 5 minutes), and runs the code (up to 10 minutes). Meanwhile the run shows Waiting, with "Running code in a sandbox", and it carries on when the code finishes. The machine is removed afterwards, and also when someone cancels the run.
  • A run started by When a request comes in answers the caller with 202 once it reaches a sandbox step, so reply from a step that runs on this server.
  • On Busabase Cloud a sandbox comes with the same plans as an AirApp's remote engine; on a plan without it, the option shows locked with the way to upgrade. On your own server, set SANDOCK_API_KEY (and SANDOCK_BASE_URL for your own Sandock) to turn it on.
  • Logs are cut at 4,000 characters. Anyone who can open the workflow can read the code, so use keys made for this purpose.

Start a run

By hand. Open the Runs tab, or press the run button in the toolbar. You can pass a Record ID for the trigger to read, and Payload JSON for any extra data. Then press Start run.

When a record changes. Under Configure:

  1. Select the Trigger and choose When a record is created or When a record is updated.
  2. Pick a Base to limit it to, or leave it on all Bases.
  3. Set Execution mode to Event, then save.

Each matching change queues one run, and the scheduler starts it. Event runs act as the person who last saved the workflow in Event mode. If that person leaves the space, those runs fail and say why.

Only when a field changes. With a Base picked, choose a field under Only when this field changes. A run then starts only when that field gets a different value, and edits to other fields start nothing. Pick a value under Changes to to start only when the field becomes that value, for example when Status becomes Ready. Editing a record that is already Ready starts nothing, and a record created as Ready counts.

On a schedule. Select the Trigger, choose On a schedule, and pick how often:

RepeatWhen it runs
Every few minutesEvery N minutes, at least 15, on the clock: every 15 minutes is :00, :15, :30, and :45.
Every few hoursEvery N hours, on the hour (UTC).
Every dayAt the time you pick.
Every weekOn the day and at the time you pick.

Day and week times are in your time zone when you save, which is shown with the next run time. Set Execution mode to Event and save. A scheduled run is not about any record, so Read the record again has nothing to read. If the scheduler was down when a run was due, it starts one run when it is back, not one for every time it missed.

When a request comes in. Select the Trigger and choose When a request comes in. The trigger gets its own Incoming URL; copy it into the service that should call you, set Execution mode to Event, and save. Every POST (JSON or text) or GET to that URL starts a run:

  • The request is in steps.trigger.payload.request: method, query, headers, and body (parsed when it is JSON). Authorization and Cookie headers are not kept.
  • The run starts at once. If a Function step returns { response: … } before the run pauses, the caller gets that answer: the value itself, or { status, contentType, body }. Text goes back as text, anything else as JSON. Otherwise the caller gets 202 with the run's ID.
  • Anyone with the URL can start a run. Change URL makes a new one and the old one stops working once you save.
  • A wrong URL, or a workflow not in Event mode, answers 404. Bodies over 256 KB answer 413, and when 200 runs are already waiting the URL answers 429.

Other event names are labels only. They do not start runs by themselves.

Examples

Every day at 10:00, ask an agent and post the answer to a WeCom group

Trigger set to On a schedule, Every day at 10:00 → a Function step askAgent → a Function step that posts to the group → End.

// "Ask the agent": call your agent's API (any HTTP API works the same way)
const res = await fetch("https://your-agent.example.com/v1/chat", {
  method: "POST",
  headers: { "content-type": "application/json", authorization: "Bearer <agent key>" },
  body: JSON.stringify({ prompt: "Summarise today's follow-ups in three lines" }),
});
const data = await res.json();
return { reply: data.reply };
// "Post to WeCom": a group robot's webhook
const res = await fetch("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<robot key>", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ msgtype: "text", text: { content: input.steps.askAgent.reply } }),
});
return { wecom: await res.json() };

A support bot that routes messages

Trigger set to When a request comes in → Condition steps.trigger.payload.request.body.text == "1" → reply step; on false, another Condition for "2" (and inside it, one more for the kind of problem), then "3" hands the question to an agent step whose answer the next step returns, and anything else gets the menu.

// A reply step: whatever you return as `response` goes back to the caller
return { response: { msgtype: "text", text: { content: "Opening hours: Mon–Fri 9:00–18:00" } } };

Every morning, remind the owner of each overdue task

Trigger set to On a schedule, Every day at 09:00 → Find records in Tasks with the View "Overdue", and Run the next steps once for each record on → a Function step that sends the reminder → Update a record with { "reminded": true } → End.

// "Remind the owner": this run is about one overdue task
const task = input.record.fields;
const res = await fetch("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<robot key>", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ msgtype: "text", text: { content: `${task.owner}: "${task.title}" is overdue` } }),
});
return { sent: res.ok };

Follow a run

The Runs tab lists the newest runs of this workflow, and Load older runs goes further back. Select a run to see each step in the order it ran, with what it saw and returned. The canvas marks the same steps as done, paused, or failed.

StatusMeaning
QueuedWaiting to start or continue. It moves on its own, usually within a minute.
RunningExecuting a step right now.
WaitingPaused on a Wait. Continue now skips the rest of the time.
Needs approvalPaused on an Approval.
CompletedReached an End step.
FailedStopped with an error. The reason is shown on the run.
CancelledSomeone stopped it. The run shows who.

Retry a failed run from the step that failed

When a run fails (a partner's server was down, a key had expired), select it and press Retry from this step. Busabase starts a new run that keeps every step before the failed one exactly as it was, and runs the failed step again with its current settings. So fix the step or the other side first, then retry.

  • Steps before the failed one are not run again, so a record they created or a message they sent is not created or sent twice. The failed step itself runs again: if it had already sent a request before it failed, that request is sent again.
  • The new run shows Retry in the list and links back to the failed run, which links forward to it. A run is retried once; if the new run fails too, retry that one in turn.
  • The button is offered only when it can work: the run failed on a step that is still in the workflow. If the step was deleted since, or the run failed after its step finished, there is nothing to retry from.
  • The new run acts as you, not as whoever started the failed run, and needs write on the workflow. Agents can do the same with busabase-cli workflows retry --run-id <id> or the workflows_retry MCP tool.

Finished runs are kept for 90 days, then deleted. Runs that have not finished are never deleted.

Approvals

An approval step that names a member by email. Only that member and space admins can decide it.

The Approver field decides who can approve or reject:

  • Member emails, separated by commas: only those members and space admins can decide. They need read access to the workflow, but not edit access. Pick members from Add a member… under the field instead of typing their emails.
  • No email (empty, or a name like "Sales lead"): anyone who can edit the workflow can decide. A name is just a label.
  • Agents can't decide unless you turn on Let agents decide. See Let agents use workflows.

When a named approver who cannot edit the workflow decides, the run goes to Queued and the scheduler continues it with the access of the person who started it, so an approver's narrower access never changes what the later steps can do.

Workflow approvals live in the Runs tab and in notifications. They are separate from the Inbox list of Change Requests. The mobile app also shows a banner for them at the top of its Inbox.

Cancel a run

Select a run that is Queued, Waiting, or Needs approval, then press Cancel run. A cancelled run cannot be continued, and steps that already ran are not undone.

A run that is executing a step cannot be cancelled until that step finishes, because a request already sent cannot be taken back.

If a bulk import queued more runs than you wanted, Cancel N queued runs at the top of the list stops all the queued ones at once. Runs that are executing a step or paused keep going.

Notifications

WhenWho hears about it
A run stops on an Approval that names peopleThose people
A run stops on an Approval that names nobodyThe person who started the run, and people subscribed to the workflow
A run failsThe person who started the run, and the workflow's creator

You get an in-app notification and an email, and the link opens that run. Whoever is pressing the button at that moment is left out, because they can already see it. Bursts are rolled up: approvals into one message per workflow every 10 minutes, failures into one per hour. The open-source single-user app has no notifications, so check the Runs tab there.

Permissions

To do thisYou need
See runs and their step dataRead access to the workflow
Start, continue early, or cancelEdit access to the workflow
Have a step update, create, or delete a recordWhat the person the run acts as may do in that Base: edit it directly, or propose a change
Approve or rejectDepends on the step, see Approvals

Function and Webhook steps send data outside Busabase, and Action steps can change your data, which is why starting a run needs edit access. Anyone who can read the workflow can see each step's data, so don't put secrets in a payload.

Settings

Select nothing on the canvas (or scroll down under Configure) to change these for the whole workflow.

SettingWhat it does
Execution modeManual starts runs only by hand. Event also lets a record Trigger or a schedule start them.
On errorStop workflow fails the run when a Function or Webhook step fails. Continue records the error and follows the success path.
ConcurrencyHow many runs of this workflow can execute steps at the same time. Runs paused on a Wait or an Approval don't count. Extra runs queue and start when a slot frees up. With several servers, two starting at the same instant can briefly go one over.
Timeout (ms)The longest a Function or Webhook step can take. A Webhook step never waits more than 30 seconds.

Webhook steps send once and do not retry, because repeating a request is not always safe. Private and internal addresses are refused, including through redirects.

Let the receiver check a request came from this step

Switch on Sign requests on a Webhook step and Busabase shows a Signing secret: copy it into the service that receives the request. Every request then carries a header the receiver can check:

X-Busabase-Signature: t=1700000000,v1=10fb0e5eb2c2204d471e7d252958fb5af663638b8d24fc8dc03090a74bcc366c

t is the time it was sent (Unix seconds) and v1 is the hex HMAC-SHA256, keyed with the secret, of the text <t>.<body>, where <body> is the request body exactly as sent (empty for a GET). The format is the same as Stripe's, so a verification snippet you already have for it works. To check a request, recompute the HMAC and compare it, and reject a t more than five minutes from now: that is how a replay is caught, because moving t forward changes the signature.

// Node.js: the receiving side. `rawBody` is the request body exactly as it arrived (a string).
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyBusabase(secret, header, rawBody, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // too old: a replay
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

// verifyBusabase("whsec_...", request.headers["x-busabase-signature"], rawBody)
# Python: the receiving side. `raw_body` is the request body exactly as it arrived (bytes).
import hashlib, hmac, time

def verify_busabase(secret: str, header: str, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance_seconds:  # too old: a replay
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
  • Change secret makes a new one at once. The old one stops verifying the requests sent after you save. Switching Sign requests off removes the secret and the header.
  • Anyone who can open the workflow can read the secret, like a header, so give each receiver its own step and secret, and change it if someone who could read it should no longer be trusted.
  • A step cannot send an X-Busabase-Signature header of its own: with signing off, no such header is sent.

A Webhook step's Headers go with every request it sends, for example Authorization: Bearer … for an API that needs a key. Anyone who can read the workflow can see them, so use a key made for this job that you can revoke. If the other site redirects the request to a different site, Authorization and Cookie are not sent on.

Let agents use workflows

Agents with an API key can use workflows through the REST API, MCP, and the CLI.

Agents canNeeds
See runs and their step dataA key that can read the workflow
Start a run, or cancel oneA write-level key, and edit access to the workflow
Approve or rejectA step that allows it, see below

Continuing a Wait early and cancelling everything queued stay in the app.

An approval is a person's decision by default. Select the step and turn on Let agents decide to allow an agent. Even then, the key must be write-level and its owner must meet the step's approver rule. The run records which key decided.

A run never outgrows the key that started it. It acts as the key's owner, limited by the key, and the run shows the key's name. Busabase checks the key again each time the run continues, so a run that is waiting stops with a clear error if you revoke the key or it expires, and it can't write if you lower the key's level.

Alerts about a run an agent started go to the key's owner too, because they aren't watching it happen.

Good to know

A run stays Queued for a long time. The workflow may be at its Concurrency, so the run is waiting for a slot. Otherwise check that the scheduler is running. It is built into self-hosted installs (configuration).

"The run was interrupted". The server stopped while a step was running. Busabase marks the run as failed instead of repeating the step, because a half-sent request must not silently run twice. Start it again.

"Could not continue as the person who started this run". That person has left the space or lost access to the workflow. Start a new run, or save the workflow in Event mode yourself so event runs act as you.

A URL answers 404. Check that Execution mode is Event, that you saved after choosing When a request comes in or Change URL, and that the whole URL was copied.

A Function step fails with a JSON error. An answer longer than 32,000 characters is cut and is no longer valid JSON. Ask the API for less, or read it with await res.text().

A schedule never starts a run. Check that Execution mode is Event and that you saved. The Trigger shows when the next run is due.

A field trigger doesn't start. The run starts only when the chosen field gets a different value. With Changes to set, a record that already has that value starts nothing until it changes away and back.

Approve and Reject don't appear. The step names other approvers, or you don't have edit access. The run tells you which rule applies.

The demo fails on Function and Webhook steps. The demo has no saved functions and sends no requests. Try those steps in your own space.

A formula fails on a step with a dash in its id. Write steps["step-a1b2c3d4"].status, not steps.step-a1b2c3d4.status. The step's Use this step in formulas as shows the right form.

See also: Node Types · Webhooks & Events · Change Requests

On this page