Vela Documentation
Reference for both Vela SDKs — the Python package on PyPI and the TypeScript package on npm (same name, two separate installs, two separate implementations of the same idea).
Both SDKs turn a plain MCP server into one with pausable, resumable, stateful workflows defined as YAML. Pick the SDK matching your server's language, read Core Concepts once (it applies to both), then jump to the Python or TypeScript section for the language-specific API.
Python
pip install vela-sdk[fastmcp] — FastMCP, LangChain, and Azure AI Agents adapters, SQLAlchemy storage.
TypeScript
npm install vela-sdk — FastMCP, official MCP SDK, LangChain, Azure AI Agents, and headless adapters.
vela-sdk on PyPI is currently registered by an unrelated project. If pip install vela-sdk doesn't resolve to this SDK for you, the package hasn't been re-published under its final name yet — check the GitHub repo for the current status.
Core Concepts
Workflow YAML
A workflow is a flat list of steps, each with a prompt the agent shows the user and an optional capture list describing what to store from the step's output. Both SDKs consume the same YAML shape.
id: cook-recipe
name: Cook a Recipe
steps:
- id: choose
type: choice
prompt: "What do you want to cook?"
options:
- key: pasta
label: Pasta Carbonara
- key: curry
label: Thai Green Curry
- id: ingredients
type: confirm
prompt: "Check these ingredients: {{state.recipe_ingredients}}"
- id: cooking
type: execute
prompt: "Follow the recipe step by step."
capture:
- key: result
elicit: never
Every step also accepts: depends_on (block advancing until named state_data fields exist), fetch (server-side data retrieval via the source registry, run once when the engine lands on the step, exposed as {{fetch.key}}), tools (tool names surfaced to the agent for this step), next (explicit next-step override), on_error (retry/fallback/abort — invoked automatically for any step type the engine executes in-process — TypeScript's delegate steps, and, in both SDKs, mcp_call steps and fetch — otherwise, for agent-executed step types, your own calling code still has to invoke it; see Known Limitations), and resources (inlined if short, referenced via a vela:// URI if long).
The Step Types
| Type | Behavior | Auto-advance |
|---|---|---|
freeform | Open-ended step, no extra fields. Captures parsed from whatever the agent outputs. | Interactive — never silently skipped |
choice | options[] (key, label, next?). Branches to the matching option's next if the output matches a key; otherwise falls back to the step's own next, then sequential order. | Interactive |
confirm | Yes/no style step; output is typically "confirmed"/"rejected". | Interactive |
execute | Agent performs a task and reports back via an explicit advance(output=...) call. instructions + a delegate string hint can be attached — see the note on delegate hints vs. delegate steps. | Always breaks — agent must call advance explicitly |
dialog | Multi-phase conversation. phases[] (explicit) or a built-in mode (brainstorming, requirements, planning, review, freeform). Each phase's output is collected; after the last phase, all outputs are merged into one markdown block before capture runs. | Always breaks |
workflow | workflow_ref + params_mapping. Pauses the parent run, auto-starts the referenced sub-workflow (params resolved from the mapping, then by matching names), and auto-resumes the parent when the child completes. | Always breaks (orchestrated one level up) |
mcp_call | mcp_tool / mcp_source / mcp_params. Calls the handler registered for mcp_source via the source registry with mcp_tool as the tool name — no agent round-trip. on_error applies automatically on failure. | Resolved in-engine, in the same advance() call |
delegate | TypeScript SDK only. Forwards execution to an app-registered handler (via a registry) synchronously, in-process — no agent round-trip. See Delegate Steps. | Resolved in-engine, in the same advance() call |
Templating
Step prompt strings (and, in TypeScript, a delegate step's task payload) resolve these variables:
{{params.x}}— a param passed when the workflow started{{steps.step_id.capture_key}}— a value captured by an earlier step{{state.x}}— anything currently in the run's state data{{fetch.x}}— the current step'sfetchresults, keyed byfetch.key(see Source Registry){{resolved.x}}— params flaggedresolve: true, sourced fromresolvedParams/resolved_paramspassed tostartOrResume/start_or_resume(the calling app resolves the value from its own context, the same way it suppliesidentityparams){{project.x}}— data for the run'sproject_id, from an app-suppliedprojectDataResolver/project_data_resolvercallback (same shape asresourceResolver: synchronous,(project_id) -> dict | None). The SDK ships no implementation — stays{}until you pass one.
Identity-Based Resume
Flag a workflow param identity: true and, when a run is started with that param set, the engine looks up any existing active/paused run with the same identity params first (store.find_by_identity / store.findByIdentity) instead of creating a new one. This is how a workflow "remembers" it's already mid-process for a given entity — e.g. the same employee_name — rather than starting a duplicate run every time.
Pausing a Run
engine.pauseRun(run, workflowDef) / engine.pause_run(run, workflow_def) explicitly parks an ACTIVE run as PAUSED. It raises if the run isn't ACTIVE, or if the workflow's lifecycle.allow_pause is false (it defaults to true, so most workflows are pausable unless they opt out). There's no separate resume method — advance() already treats PAUSED the same as ACTIVE, so calling it again is the resume path.
Lifecycle Checks
A workflow's lifecycle block declares two duration-based rules — auto_archive_after (e.g. "30d") and auto_cancel_after (e.g. "90d") — evaluated against a run's updated_at/updatedAt timestamp. engine.checkLifecycle(run, workflowDef.lifecycle) / engine.check_lifecycle(run, workflow_def.lifecycle) returns the status a run should transition to (CANCELLED for an ACTIVE run past auto_cancel_after, ARCHIVED for a COMPLETED run past auto_archive_after), or None/null if nothing applies.
checkLifecycle/check_lifecycle is a pure check — neither SDK runs a background sweep or scheduler. If you want auto_archive_after/auto_cancel_after enforced, your own code has to call it (e.g. on a cron, or lazily whenever a run is loaded) and persist the returned status yourself via store.update_step/updateStep. See Known Limitations.
Python SDK
Package: packages/vela-sdk/ · distributed on PyPI as vela-sdk.
Installation
pip install vela-sdk[fastmcp] # FastMCP integration (most common) pip install vela-sdk[sqlalchemy] # SQL-backed WorkflowStore pip install vela-sdk[langchain] # LangChain VelaToolkit pip install vela-sdk[azure-agents] # Azure AI Agents VelaToolset pip install vela-sdk[all] # everything
VelaWorkflows
Positional mcp argument, plus a workflows_dir that's recursively scanned for YAML files at construction time.
from fastmcp import FastMCP
from vela_sdk import VelaWorkflows
mcp = FastMCP("cooking-assistant")
workflows = VelaWorkflows(mcp, workflows_dir="./workflows/")
| Param | Type | Default | Purpose |
|---|---|---|---|
mcp | FastMCP | required | The server instance to register tools/prompts on. |
store | WorkflowStore | InMemoryStore() | Where run state lives. |
workflows_dir | str | list[str] | None | Directory (or directories) recursively scanned for workflow YAML, ~ expanded. |
initial_workflows | dict[str, WorkflowDefinition] | None | Pre-parsed workflows, keyed "id@version". |
resource_resolver | Callable[[str], ResourceDefinition | None] | None | Custom resource lookup. |
tool_prefix | str | "workflow" | Prefix for the 3 registered tools. |
tool_name_format | dict[str, str] | {} | Per-tool name override — keys advance/status/list. |
auto_advance | bool | True | Loop elicit → advance automatically until an interactive step is hit. |
register_prompts | bool | True | Also register one MCP prompt per workflow. |
workflow_resolver | WorkflowResolver | InMemoryWorkflowResolver | See Extension Points. |
session_provider | SessionProvider | SimpleSessionProvider | See Extension Points. |
param_filter | ParamFilter | DefaultParamFilter | See Extension Points. |
project_resolver | ProjectResolver | None | See Extension Points. |
locale | Locale | Locale.en() | See Locale & Tool Names. |
workflows.register(workflow) adds a workflow definition at runtime after construction.
Extension Points
Four Protocol classes (fastmcp/protocols.py), each swappable via a keyword argument to VelaWorkflows. Shared with the LangChain and Azure adapters, except ParamFilter/ProjectResolver, which are FastMCP-only (those adapters don't elicit params).
| Protocol | Method(s) | Why implement one |
|---|---|---|
WorkflowResolver | async get_workflow(workflow_id, version=None)async list_workflows() | Load workflows from a database or remote source instead of a static YAML directory. |
SessionProvider | session() -> AsyncContextManager[WorkflowStore] | Manage a store's lifecycle per call — e.g. one DB session per request. |
ParamFilter | filter_missing_params(wf_def, provided_params) | Control which params trigger the elicitation dialog when a workflow starts. |
ProjectResolver | async resolve_project_id(project_slug=None) | Only if your server has a project concept — resolves a slug to an ID for the advance tool. |
class DatabaseWorkflowResolver:
async def get_workflow(self, workflow_id, version=None):
return await db.fetch_workflow(workflow_id, version)
async def list_workflows(self):
return await db.fetch_all_workflows()
workflows = VelaWorkflows(mcp, workflow_resolver=DatabaseWorkflowResolver())
Storage
The WorkflowStore protocol has 6 async methods: find_by_identity, create_run, update_step, get_by_id, list_active, commit.
| Store | Backend | Notes |
|---|---|---|
InMemoryStore | Python dict | Default. Zero dependencies, no persistence across restarts. |
SQLAlchemyStore | Async SQLAlchemy ORM | Own standalone workflow_runs table (no foreign keys into any other schema). Three init modes: session=, session_factory=, or database_url= (builds its own async engine). Call await store.ensure_tables() once to create the table. |
Custom stores just implement the 6-method protocol directly — no base class required.
Adapters
LangChain — VelaToolkit
from vela_sdk.langchain import VelaToolkit toolkit = VelaToolkit(workflows_dir="./workflows/") tools = toolkit.get_tools() # 3 BaseTool instances: advance, status, list
Thinner than the FastMCP integration — LangChain tools have no elicitation primitive, so params/captures aren't auto-elicited.
Azure AI Agents — VelaToolset
from vela_sdk.azure_agents import VelaToolset toolset = VelaToolset(workflows_dir="./workflows/") functions = toolset.get_functions() # set[Callable], for Azure's auto schema-gen tool_set = toolset.get_toolset() # azure.ai.agents.models.ToolSet instructions = toolset.get_prompt_advisor() # markdown blurb for additional_instructions=
Requires the optional azure-ai-agents dependency (pip install vela-sdk[azure-agents]). Mirrors the LangChain toolkit's structure and the same extension points.
Locale & Tool Names
from vela_sdk.locale import Locale workflows = VelaWorkflows(mcp, locale=Locale.de()) # or partial override: custom = dataclasses.replace(Locale.en(), confirm_prompt="Bitte bestätigen?")
tool_prefix renames all 3 tools at once ({prefix}_advance/status/list); tool_name_format={"advance": "do_step"} overrides them individually.
The locale= you pass to VelaWorkflows/FastMcpIntegration covers both the FastMCP integration layer's own strings (the next_action text, elicitation prompts) and the workflow engine's own step-prompt assembly and dialog-phase headers — WorkflowEngine.assemble_prompt/.advance accept a locale parameter too, defaulting to English when omitted.
TypeScript SDK
Package: packages/vela-sdk-ts/ · distributed on npm as vela-sdk — a separate implementation from the Python package of the same name, not a port sharing code.
Installation
npm install vela-sdk
Adapters are separate subpath exports so you only pull in what you use:
import { FastMcpAdapter } from "vela-sdk/adapters/fastmcp";
import { OfficialSdkAdapter } from "vela-sdk/adapters/mcp-sdk";
import { createVelaToolkit } from "vela-sdk/adapters/langchain";
import { createVelaAzureToolset } from "vela-sdk/adapters/azure-agents";
import { HeadlessAdapter } from "vela-sdk"; // main entry, no subpath
VelaWorkflows
A single options object — not a positional mcp argument. workflows is an array of YAML strings, not a directory path to scan.
import { FastMCP } from "fastmcp";
import { VelaWorkflows } from "vela-sdk";
import { FastMcpAdapter } from "vela-sdk/adapters/fastmcp";
const server = new FastMCP({ name: "my-server", version: "1.0.0" });
const vela = new VelaWorkflows({
server: new FastMcpAdapter(server),
workflows: [myWorkflowYaml],
});
server.start({ transportType: "stdio" });
| Option | Type | Default |
|---|---|---|
server | McpServerAdapter | new HeadlessAdapter() |
store | WorkflowStore | new InMemoryStore() |
workflows / agents / resources | string[] (YAML) | [] |
toolPrefix | string | "workflow" |
toolNameFormat | Record<string,string> | {} |
locale | Locale | English |
autoAdvance | boolean | true |
registerPrompts | boolean | true |
workflowResolver | WorkflowResolver | InMemoryWorkflowResolver |
sessionProvider | SessionProvider | SimpleSessionProvider |
paramFilter | ParamFilter | DefaultParamFilter |
projectResolver | ProjectResolver | undefined |
resourceResolver | ResourceResolver | undefined |
vela.register(workflow) adds a workflow at runtime; vela.buildWorkflowPrompt(wf, advanceName, locale, ctx) is a public, reusable prompt builder for custom bridging.
Extension Points
Same four concepts as the Python SDK, same names, TypeScript interfaces instead of Python protocols (src/mcp/protocols.ts):
| Interface | Method(s) | Default |
|---|---|---|
WorkflowResolver | getWorkflow(id, version?), listWorkflows() | InMemoryWorkflowResolver |
SessionProvider | session(): AsyncSession — { store, close() }, manual dispose | SimpleSessionProvider |
ParamFilter | filterMissingParams(wfDef, providedParams) | DefaultParamFilter |
ProjectResolver | resolveProjectId(projectSlug?) | none — project_slug ignored if unset |
Storage
| Store | Backend | Notes |
|---|---|---|
InMemoryStore | JS Map | Default. No persistence. |
LocalStorageStore | Any KVStorage | new LocalStorageStore(storage, prefix = "vela:"). KVStorage is a minimal getItem/setItem/removeItem interface (sync or async) — compatible with browser localStorage, node-localstorage, or a Redis/Cloudflare-KV wrapper you write. Persists each run as vela:run:<id> plus a vela:index array for scans. |
SQLAlchemyStore, there is no ORM-backed store in the TypeScript SDK today. Implement WorkflowStore directly (6 methods, no base class) if you need one.
Adapters
| Adapter | Elicitation | Use case |
|---|---|---|
FastMcpAdapter | Not supported — ctx.elicit throws | Bridges the fastmcp npm package. |
OfficialSdkAdapter | Full support via server.elicitInput | Bridges @modelcontextprotocol/sdk's McpServer. |
HeadlessAdapter | N/A — no transport | Default when no server is given. Stores tools/prompts in-memory (getTools()/getPrompts()) for custom bridging — Electron IPC, an Agent SDK's own MCP layer, etc. |
LangChainAdapter | Declines (agent-driven fallback) | Via createVelaToolkit() — LangChain.js DynamicStructuredTools. |
AzureAgentsAdapter | Declines (agent-driven fallback) | Via createVelaAzureToolset() — Azure AI Foundry native function-tool format (Azure has no MCP support). |
import { createVelaToolkit } from "vela-sdk/adapters/langchain";
const { tools, vela } = createVelaToolkit({ workflows: [myWorkflowYaml] });
const agent = createReactAgent({ llm, tools });
import { createVelaAzureToolset } from "vela-sdk/adapters/azure-agents";
const { tools, handleToolCall, promptAdvisor } =
createVelaAzureToolset({ workflows: [myWorkflowYaml] });
// pass `tools` as the agent's function tool defs
// dispatch calls via handleToolCall(name, args)
// inject `promptAdvisor` into the agent's system instructions
Delegate Steps — TypeScript only, new
A delegate step forwards execution to an app-registered handler, resolved synchronously and in-process — no agent round-trip, unlike every other step type. Register a handler once at startup:
import { registerDelegate } from "vela-sdk";
registerDelegate("shell", async (step, ctx) => {
const task = ctx.resolveVars(step.task); // resolves {{...}} templates deep in the object
const result = await runShellCommand(task.command);
ctx.setCapture("exit_code", result.code);
return { output: result.stdout };
});
Referenced from YAML by name:
- id: run-shell
type: delegate
delegate: shell
task:
command: "echo {{params.name}}"
capture:
- key: exit_code
source: output
The handler signature: (step: { id, delegate, task }, ctx: DelegateContext) => Promise<unknown>, where DelegateContext gives you resolveVars, setCapture, an AbortSignal, and a no-op-by-default log. registerDelegate(name, handler) throws if the name is already taken; resolveDelegate(name) and clearDelegates() (test-only) round out the registry API. A non-object handler return is wrapped as { result: value }; values written via ctx.setCapture win over same-key values from the return object.
execute step's delegate field. execute steps have long supported an optional delegate/instructions pair that's just a string label surfaced to the caller via the advance response (AdvanceResult.delegate) — it tells the calling agent "hand this off to a subagent named X", but nothing in the SDK resolves or invokes it. The delegate step type described here is fully resolved and executed by the SDK itself via the registry. Same word, two unrelated mechanisms.
Source Registry (mcp_call & fetch) — both SDKs, new
Neither engine is a real MCP client — there's no protocol handshake to a "mounted server". Instead, mcp_call steps and the fetch step field both resolve against the same named-callback registry: the embedding app registers a handler per source namespace (e.g. "devops") once at startup, and the engine looks it up and calls it in-process — the same shape as the delegate registry above, just keyed by mcp_source/fetch.source instead of delegate.
import { registerSource } from "vela-sdk";
registerSource("devops", async (tool, params, ctx) => {
if (tool === "get-status") return callInternalApi(params);
throw new Error(`unknown tool: ${tool}`);
});
from vela_sdk import register_source
async def devops_handler(tool, params, ctx):
if tool == "get-status":
return await call_internal_api(params)
raise ValueError(f"unknown tool: {tool}")
register_source("devops", devops_handler)
An mcp_call step runs its handler once and auto-advances — no agent round-trip, same in-engine execution (and automatic on_error retry/fallback/abort) as delegate:
- id: check-status
type: mcp_call
mcp_source: devops
mcp_tool: get-status
mcp_params:
service: "{{params.service_name}}"
capture:
- key: status
source: output
fetch is different: it's a field on every step type, run once when the engine lands on that step (before its prompt is shown), and exposes its results as {{fetch.key}} rather than driving the step's own advancement:
- id: review
type: freeform
prompt: "Current status: {{fetch.status}}. What should we do?"
fetch:
- key: status
source: devops
action: get-status
params:
service: "{{params.service_name}}"
Handler signature: (tool, params, ctx) => Promise<unknown> / async def handler(tool, params, ctx), where params already has {{...}} templates resolved and ctx gives you a cancellation signal and a no-op-by-default log. registerSource/register_source raises if the name is already taken; resolveSource/resolve_source and clearSources/clear_sources (test-only) round out the API. Both mcp_call and fetch apply the landing/current step's own on_error on failure — retry, then fallback to another step or abort (cancel) the run, exactly like delegate steps.
Locale & Tool Names
import { getLocale } from "vela-sdk";
const vela = new VelaWorkflows({
server: adapter,
workflows: [yaml],
locale: getLocale("de"),
});
Same toolPrefix / toolNameFormat mechanism as the Python SDK (keys advance/status/list). As in Python, this locale covers both the MCP adapter layer's strings and the workflow engine's own step-prompt assembly — assemblePrompt/advance on DefaultWorkflowEngine accept a locale parameter too, defaulting to English.
Known Limitations
Documented honestly rather than silently — these are gaps that remain a deliberate design choice or an architectural boundary, not a to-do. (Older entries about {{resolved.x}}, locale, and allow_pause have been folded into Templating, Locale & Tool Names, and Pausing a Run — they're fully implemented now, not limitations.)
-
Both SDKs
on_error(retry/fallback/abort) only auto-applies for step types the engine executes in-process: TypeScript'sdelegatestep, and, in both SDKs,mcp_calland thefetchstep field. For step types the calling agent executes (execute,freeform,choice,confirm, dialog phases,workflow), the engine never runs the risky action itself — there's no way for it to, since those steps hand control back to a human/agent turn — so your own calling code still has to invokeon_errorand act on the result. This isn't a gap that will close; it's inherent to which step types run synchronously in-engine versus round-trip through the caller. -
Both SDKs
checkLifecycle/check_lifecycle(Lifecycle Checks) is a pure function, not a scheduler — neither engine sweeps runs in the background to applyauto_archive_after/auto_cancel_after. Your own code has to call it and persist the result. This is a deliberate scope boundary (the SDK has no process to run a background job in), not something planned to change. -
Both SDKs
{{project.x}}only resolves if you supply aprojectDataResolver/project_data_resolver— the SDK has no project data store of its own (that lives in the separate, closed-source Vela Server product). Without one, it stays{}rather than raising. See Templating.