Workflows
Chain multiple Botverse tools into a single declarative pipeline. Define your steps in JSON, submit once, and poll for results — the engine handles parallelism, dependencies, retries, and failure recovery.
How it works
A workflow is a JSON document written in BWDL — the Botverse Workflow Definition Language. You submit it with submit_workflow, which validates the definition, charges a billing gate, and immediately returns a workflow_id. Then you poll get_workflow_statusevery 5–10 seconds: each call advances the workflow — it checks in-progress steps, dispatches newly ready ones, and applies failure policies. When a terminal status is returned, the workflow is done.
Quick start
The simplest possible workflow — one step, no dependencies. Once you understand the submit-and-poll pattern, multi-step pipelines work exactly the same way.
// Call submit_workflow with your BWDL definition
const result = await callTool("submit_workflow", {
definition: {
workflow_id: "my-first-workflow-001",
steps: [
{
id: "transcode",
tool: "transcode_from_url",
inputs: {
source_url: "https://example.com/video.mp4",
output_format: "mp4"
}
}
]
}
});
// Returns immediately
// { workflow_id: "my-first-workflow-001", status: "PROCESSING", step_count: 1 }let status;
do {
await sleep(5000);
status = await callTool("get_workflow_status", {
workflow_id: "my-first-workflow-001"
});
} while (!["COMPLETED", "FAILED", "PARTIALLY_FAILED", "CANCELLED"].includes(status.status));
// Terminal — inspect step outputs
const step = status.steps.find(s => s.step_id === "transcode");
console.log(step.status); // "COMPLETED"
console.log(step.output_url); // pre-signed download URL
console.log(step.cost_usd); // e.g. 0.001BWDL schema reference
A BWDL document is a JSON object. The $schema field is optional but enables validation in editors that support JSON Schema.
Top-level fields
Step fields
Available tools
The tool field must be one of these values:
Transcode output_format values: mp4 webm mov_prores mp3 gif. Convert output_format values: docx pdf html txt md rst xlsx. Transcribe output_format values: txt json srt vtt docx pdf. Pass transcribe attendees and other settings via a nested options object in the step's inputs.
Expression syntax
Step inputs support JSONPath-style expressions that are resolved at dispatch time. Any input value that starts with $. is treated as an expression; all other values are used as-is.
$.steps.{id}.output_key as the value of a source_url input, the engine automatically substitutes the pre-signed download URL for that step — you don't need to handle URL generation yourself.Parallelism and dependencies
Steps with no depends_on (or an empty array) are dispatched immediately when the workflow starts. Steps that list one or more dependencies wait until every listed step reaches COMPLETED before they dispatch. This makes fan-out and fan-in natural to express.
A step can depend on multiple steps — it will wait until all of them complete (fan-in). Circular dependencies are detected at submit time and rejected with a CIRCULAR_DEPENDENCY error.
Conditional steps
A step with a when field is only dispatched if the condition evaluates to true. If it evaluates to false, the step is marked SKIPPED — it does not block downstream steps that depend on it.
Supported condition syntax:
{
"id": "prores_master",
"tool": "transcode_from_url",
"depends_on": ["ingest"],
"when": "$.params.include_prores == 'true'",
"inputs": {
"source_url": "$.steps.ingest.output_key",
"output_format": "mov_prores"
}
}Failure modes
Each step can declare how the workflow behaves if that step fails. The default is HALT.
{
"id": "upload_to_cdn",
"tool": "transcode_from_url",
"failure_mode": "COMPENSATE",
"on_failure_compensate": "cleanup",
"inputs": { "source_url": "$.params.source_url", "output_format": "mp4" }
},
{
"id": "cleanup",
"tool": "convert_content",
"inputs": {
"content": "Cleanup triggered for $.params.job_ref",
"input_format": "txt",
"output_format": "txt"
}
}Retry policy
"retry": {
"max_attempts": 3, // 1–10. Counts the original attempt — so 3 = 2 retries.
"initial_interval_seconds": 5,// Wait before first retry. Default: 1.
"backoff_coefficient": 2.0, // Multiply interval on each retry. Default: 1 (no backoff).
"retryable_errors": [ // Optional — only retry these error substrings.
"FETCH_FAILED",
"TIMEOUT"
]
}Status values
Workflow statuses
Step statuses
MCP tool reference
submit_workflowValidate and submit a BWDL workflow definition. Returns immediately — the workflow runs server-side. Submitting the same workflow_id twice is idempotent: the existing workflow is returned without re-running.
| Name | Type | Req | Description |
|---|---|---|---|
| definition | object | yes | BWDL workflow definition. Must include workflow_id (string) and steps (array). Each step requires id, tool, and inputs. |
| Field | Type | Description |
|---|---|---|
| workflow_id | string | The workflow_id from your definition. Pass this to get_workflow_status. |
| status | string | Initial status — always PROCESSING on a new submission. |
| step_count | number | Number of steps in the workflow. |
| already_exists | boolean | true if this workflow_id was already submitted. |
get_workflow_statusAdvance and return the current state of a workflow. Each call checks in-progress steps, dispatches newly ready steps, and applies failure policies. Poll every 5–10 seconds until a terminal status is returned.
| Name | Type | Req | Description |
|---|---|---|---|
| workflow_id | string | yes | Workflow ID returned by submit_workflow. |
| Field | Type | Description |
|---|---|---|
| workflow_id | string | Echoed back. |
| status | string | Current workflow status. |
| total_cost_usd | number | Cumulative cost of all completed steps. |
| error_message | string | Set when the workflow reaches FAILED. |
| completed_at | string | ISO timestamp when the workflow reached a terminal state. |
| steps | array | Array of step state objects — see fields below. |
Each object in steps contains:
cancel_workflowCancel an in-progress workflow. All QUEUED and DISPATCHED steps are immediately marked CANCELLED. Has no effect on already-terminal workflows — calling it on a COMPLETED or FAILED workflow returns an error.
| Name | Type | Req | Description |
|---|---|---|---|
| workflow_id | string | yes | Workflow ID to cancel. |
| Field | Type | Description |
|---|---|---|
| workflow_id | string | Echoed back. |
| cancelled | boolean | Always true on success. |
| status | string | Always CANCELLED on success. |
Error codes
Validation errors from submit_workflow include a machine-readable codein the JSON-RPC error object at error.data.code.
Complete example — multi-rendition pipeline
This pipeline takes a source URL from params, ingests it as an MP4, then fans out to three parallel renditions: H.264 MP4 for web, VP9 WebM for modern browsers, and an MP3 audio track. The three output steps all depend on the ingest step and run concurrently.
{
"$schema": "https://botverse.cloud/schemas/workflow/v1.json",
"workflow_id": "multi-rendition-20240610-001",
"version": "1.0",
"description": "Transcode one source URL into MP4, WebM, and MP3",
"params": {
"source_url": "https://example.com/video.mp4"
},
"steps": [
{
"id": "ingest",
"tool": "transcode_from_url",
"description": "Fetch and store the source file as baseline MP4",
"inputs": {
"source_url": "$.params.source_url",
"output_format": "mp4"
}
},
{
"id": "mp4",
"tool": "transcode_from_url",
"description": "H.264 MP4 for web delivery",
"depends_on": ["ingest"],
"inputs": {
"source_url": "$.steps.ingest.output_key",
"output_format": "mp4"
},
"retry": { "max_attempts": 2 }
},
{
"id": "webm",
"tool": "transcode_from_url",
"description": "VP9 WebM for modern browsers",
"depends_on": ["ingest"],
"failure_mode": "SKIP_DEPENDENTS",
"inputs": {
"source_url": "$.steps.ingest.output_key",
"output_format": "webm"
}
},
{
"id": "audio",
"tool": "transcode_from_url",
"description": "MP3 audio track",
"depends_on": ["ingest"],
"failure_mode": "CONTINUE",
"inputs": {
"source_url": "$.steps.ingest.output_key",
"output_format": "mp3"
}
}
]
}Submitting and polling this workflow
// Submit
const { workflow_id } = await callTool("submit_workflow", { definition });
// Poll
let result;
do {
await new Promise(r => setTimeout(r, 5000));
result = await callTool("get_workflow_status", { workflow_id });
} while (!["COMPLETED","FAILED","PARTIALLY_FAILED","CANCELLED"].includes(result.status));
// Collect outputs
for (const step of result.steps) {
console.log(step.step_id, step.status, step.output_url);
}
// mp4 COMPLETED https://storage.botverse.cloud/...?X-Amz-...
// webm COMPLETED https://storage.botverse.cloud/...?X-Amz-...
// audio COMPLETED https://storage.botverse.cloud/...?X-Amz-...output_url are valid for 1 hour. Download and store the files to long-term storage before the URL expires.Worked example — meeting recording to a transcript pack
A genuinely fire-and-forget job: hand it a recording URL and an attendee list, and it returns a named Word transcript, a PDF of that transcript, and an MP3 of the audio — one submit, one poll loop, no agent involvement in between. Transcribe and the audio extraction run in parallel; the PDF waits on the transcript.
{
"workflow_id": "transcript-pack-20260622-001",
"description": "Recording -> named Word transcript + PDF + MP3 audio",
"params": { "recording_url": "https://storage.example.com/all-hands.mp4" },
"steps": [
{
"id": "transcribe",
"tool": "transcribe_from_url",
"description": "Speaker-named Word transcript",
"inputs": {
"source_url": "$.params.recording_url",
"output_format": "docx",
"options": {
"attendees": [
{ "name": "Sarah Chen", "role": "host" },
{ "name": "Mike Torres" },
{ "name": "Priya Patel" }
]
}
}
},
{
"id": "audio",
"tool": "transcode_from_url",
"description": "MP3 of the meeting audio (runs in parallel)",
"failure_mode": "CONTINUE",
"inputs": {
"source_url": "$.params.recording_url",
"output_format": "mp3"
}
},
{
"id": "pdf",
"tool": "convert_from_url",
"description": "PDF rendered from the Word transcript",
"depends_on": ["transcribe"],
"inputs": {
"source_url": "$.steps.transcribe.output_key",
"output_format": "pdf"
}
}
]
}Worked example — keyword highlight clips
A common request: “transcribe this video, and whenever someone says a keyword, cut a 30-second audio clip there and drop the clips plus the transcript in a folder.” This is the showcase for long, self-running jobs — but it has a subtlety worth understanding, because it shapes how you build it.
So the job runs in two phases, both backed by Botverse:
Phase 1 — transcribe, then find the moments
// 1. Transcribe to JSON (one tool call, or a one-step workflow)
const { job_id } = await callTool("transcribe_from_url", {
source_url: recordingUrl,
output_format: "json",
options: { attendees }
});
const transcript = await pollAndFetchJson(job_id); // [{ start, end, speaker, text }, ...]
// 2. The agent decides where to cut — this is the part a static graph cannot do
const KEYWORD = "budget";
const CLIP = 30; // seconds
const hits = transcript
.filter(seg => seg.text.toLowerCase().includes(KEYWORD))
.map(seg => ({ start: Math.max(0, Math.floor(seg.start) - 2) }));Phase 2 — generate a fan-out workflow from the hits
The agent builds one workflow with a clip step per hit (each trims the source withstart_time + duration) plus a transcript DOCX, submits it once, and polls. Every clip runs in parallel.
const definition = {
workflow_id: "highlights-" + Date.now(),
description: "30s MP3 clips at every '" + KEYWORD + "' mention, plus the transcript",
params: { recording_url: recordingUrl },
steps: [
// one clip per keyword hit — all parallel, soft-fail so one bad cut never sinks the batch
...hits.map((h, i) => ({
id: "clip_" + i,
tool: "transcode_from_url",
description: "30s audio clip at " + h.start + "s",
failure_mode: "CONTINUE",
inputs: {
source_url: "$.params.recording_url",
output_format: "mp3",
options: { start_time: String(h.start), duration: 30 }
}
})),
// the transcript document, alongside the clips
{
id: "transcript",
tool: "transcribe_from_url",
inputs: {
source_url: "$.params.recording_url",
output_format: "docx",
options: { attendees }
}
}
]
};
const { workflow_id } = await callTool("submit_workflow", { definition });
// 3. Poll to terminal, then save every artifact to the folder
let result;
do {
await new Promise(r => setTimeout(r, 8000));
result = await callTool("get_workflow_status", { workflow_id });
} while (!["COMPLETED","FAILED","PARTIALLY_FAILED","CANCELLED"].includes(result.status));
for (const step of result.steps) {
if (step.output_url) await download(step.output_url, "./highlights/");
}whencondition can branch on a step's status, but not on the contents of a transcript. Pushing the keyword scan onto the agent keeps the engine simple and deterministic, and keeps the intelligence where it belongs. Phase 2 is still a single self-running job — submit once, walk away, collect the folder.