Workflows as Agent Tools
Give any AI agent reliable crypto skills by exposing B3OS workflows as tool calls — mint a scoped key, run a workflow, track it to completion, and read run history over the REST API.
B3OS workflows are durable, auditable automations that move real value on-chain, call dozens of integrations, and wait for human approval when needed. Any of them can be exposed to an AI agent as a tool call: your agent hands B3OS a payload, B3OS runs the workflow, and your agent tracks the run to completion and reads the result.
This guide is the end-to-end path for an agent builder: mint a least-privilege key, run a published workflow, follow it to a terminal status, and list run history — all over the REST API, from any agent framework.
To create workflows from code (define → validate → publish), see the API Quickstart. This page is about invoking and tracking workflows your org already owns, which is what an agent does at runtime.
The loop
Mint a scoped key
Create a service account with only the permissions an agent needs, then mint an API key for it.
Run a published workflow
POST /v1/workflows/:id/run with a payload. Returns a runId immediately — execution is asynchronous.
Track it to a terminal status
Poll GET /v1/runs/:id, or stream GET /v1/runs/:id/stream (SSE), until the run reaches success, partial,
failure, or cancelled.
Read the result and history
Read per-node outputs from executionState, and list prior runs with GET /v1/workflows/:id/runs.
1. Mint a least-privilege key
API keys use the b3sk_ prefix and are sent as Authorization: Bearer b3sk_.... A key is permanently bound to its organization — it can never read or run another org's data.
For an agent, prefer a service account over a coarse read-write user key. A service account carries an explicit permission list, so you can grant exactly "run workflows + read runs" and nothing else. (A read-write user key also holds apikey:create/apikey:revoke, so it can mint more keys — avoid handing that to an agent.)
Create the service account
bashcurl -sS -X POST "$B3OS_API_URL/v1/service-accounts" \ -H "Authorization: Bearer $B3OS_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "trading-agent", "description": "Runs market + swap workflows for the trading agent", "permissions": ["workflow:read", "workflow:execute", "run:read"] }'
You can only grant permissions you hold yourself. workflow:read + workflow:execute + run:read is the complete set for the agent loop — discover, run, poll, stream, cancel, and read history. No extra permissions are needed: SSE streaming is gated on run:read, and cancelling a run is gated on workflow:execute, both already in the set.
Mint a key for it
bashcurl -sS -X POST "$B3OS_API_URL/v1/api-keys" \ -H "Authorization: Bearer $B3OS_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "trading-agent key", "serviceAccountId": "sa_123" }'
The plaintext key (key in the response) is shown once — store it in a secret manager. Hand this key to the agent; keep B3OS_ADMIN_KEY (the owner/developer key that created the account) on your provisioning path only.
Today a service account and its key are created by an existing org owner or developer (in the B3OS app or via the API above). There is no anonymous "sign up and get a key" flow — an agent always acts inside a B3OS organization that someone provisioned. Plan your integration as "my org runs my workflows on behalf of my users."
2. Run a published workflow
The workflow must be published (status active). Pass run-time inputs in payload; everything is optional.
bashcurl -sS -X POST "$B3OS_API_URL/v1/workflows/$WORKFLOW_ID/run" \ -H "Authorization: Bearer $B3OS_AGENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "payload": { "asset": "USDC", "amount": "100" } }'
json{ "data": { "runId": "run_c9i6j8k2l0m3" }, "code": 200, "message": "success" }
The call returns as soon as the run is queued — it does not wait for completion. Useful options in the body:
| Field | Type | Purpose |
|---|---|---|
payload | object | Run-time inputs, read inside the workflow via {{$trigger.body.*}} |
runId | string | Pre-choose the run id (must start run_) so you can open the SSE stream before running |
simulate | boolean | Dry-run — executes logic without sending on-chain transactions |
version | integer | Run a specific published version instead of the default |
triggerNodeId | string | For multi-trigger workflows, which trigger to attribute the run to |
A manual workflow also exposes an unauthenticated webhook: POST /v1/workflows/:id/webhook?secret=..., where the secret comes from GET /v1/workflows/:id/webhook-secret. It fires a run and returns only { action, runId } — handy for "trigger and forget", but it exposes no run internals, so use the authenticated path above when your agent needs the result.
3. Track the run to a terminal status
A run's status is one of running, waiting, success, partial, failure, or cancelled. The last four are terminal. (waiting means a node is paused — e.g. a human-approval or wait-for-event gate.)
Poll
bashcurl -sS "$B3OS_API_URL/v1/runs/$RUN_ID?summary=true" \ -H "Authorization: Bearer $B3OS_AGENT_KEY"
?summary=true drops the heavy executionState and workflowDefinition so a status poll stays light; omit it (or pass ?nodeIds=a,b) when you want per-node results. Poll until status is terminal, then read the result (below).
Stream (SSE)
For push updates instead of polling, open the Server-Sent Events stream. It needs run:read (a read-scope key can stream). The stream closes itself when the run completes.
bashcurl -N "$B3OS_API_URL/v1/runs/$RUN_ID/stream" \ -H "Authorization: Bearer $B3OS_AGENT_KEY"
javascriptconst res = await fetch(`${process.env.B3OS_API_URL}/v1/runs/${runId}/stream`, { headers: { Authorization: `Bearer ${process.env.B3OS_AGENT_KEY}` },});for await (const chunk of res.body) process.stdout.write(Buffer.from(chunk));
pythonimport os, requestswith requests.get( f"{os.environ['B3OS_API_URL']}/v1/runs/{run_id}/stream", headers={"Authorization": f"Bearer {os.environ['B3OS_AGENT_KEY']}"}, stream=True,) as r: for line in r.iter_lines(): if line: print(line.decode())
To guarantee you see run_started, choose the runId yourself, open the stream first, then run with that same
runId in the body. The stream lets you subscribe to a run id before it exists.
Read the result
GET /v1/runs/:id returns the full run, including executionState — a map of nodeId → { status, input, result, startedAt, finishedAt }. Read the result of the node whose output your agent needs; that's the tool's return value. Secrets are masked in the response.
4. Read run history
| Want | Endpoint |
|---|---|
| Runs for one workflow | GET /v1/workflows/:id/runs |
| All runs in the org | GET /v1/runs |
| Counts grouped by status | GET /v1/runs/status-counts |
All list endpoints take limit (default 20, max 100), offset, and an optional status filter, and return { data: { items, limit, offset, hasMore } }. There is no total — page with offset until hasMore is false.
Permissions at a glance
Grant a service account exactly the rows the agent uses. (For a legacy user key, read scope covers the read/stream rows but not workflow:execute; read-write covers everything.)
| Operation | Endpoint | Permission | read key | read-write key |
|---|---|---|---|---|
| Run a workflow | POST /v1/workflows/:id/run | workflow:execute | — | ✅ |
| Get a run / its result | GET /v1/runs/:id | run:read | ✅ | ✅ |
| List run history | GET /v1/runs, .../runs | run:read | ✅ | ✅ |
| Stream a run (SSE) | GET /v1/runs/:id/stream | run:read | ✅ | ✅ |
| Cancel a run | POST /v1/runs/:id/cancel | workflow:execute | — | ✅ |
| Discover workflows | GET /v1/workflows | workflow:read | ✅ | ✅ |
Wire it into your agent
Expose each workflow as one tool. The tool's input schema mirrors the workflow's manual-trigger inputSchema; the implementation is the run-then-track loop above.
json{ "name": "run_treasury_rebalance", "description": "Rebalance the org treasury to target allocation. Returns the per-swap results.", "input_schema": { "type": "object", "properties": { "maxSlippageBps": { "type": "integer", "description": "Max slippage in basis points" } } }}
texton tool call(args): runId = POST /v1/workflows/{id}/run { payload: args } # → run_... loop: run = GET /v1/runs/{runId}?summary=true if run.status in {success, partial, failure, cancelled}: break sleep(1s) # or use the SSE stream full = GET /v1/runs/{runId} # read executionState[node].result return summarize(full)
If your agent speaks MCP, the B3OS MCP server wraps these same endpoints as tools so Claude can discover, run, and inspect workflows without hand-writing HTTP. The REST path on this page is the framework-agnostic equivalent and the source of truth for scopes.
