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.

Note: 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

TypeBehaviorAuto-advance
freeformOpen-ended step, no extra fields. Captures parsed from whatever the agent outputs.Interactive — never silently skipped
choiceoptions[] (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
confirmYes/no style step; output is typically "confirmed"/"rejected".Interactive
executeAgent 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
dialogMulti-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
workflowworkflow_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_callmcp_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
delegateTypeScript 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's fetch results, keyed by fetch.key (see Source Registry)
  • {{resolved.x}} — params flagged resolve: true, sourced from resolvedParams/resolved_params passed to startOrResume/start_or_resume (the calling app resolves the value from its own context, the same way it supplies identity params)
  • {{project.x}} — data for the run's project_id, from an app-supplied projectDataResolver/project_data_resolver callback (same shape as resourceResolver: 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.

You call this, not the engine. 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/")
ParamTypeDefaultPurpose
mcpFastMCPrequiredThe server instance to register tools/prompts on.
storeWorkflowStoreInMemoryStore()Where run state lives.
workflows_dirstr | list[str]NoneDirectory (or directories) recursively scanned for workflow YAML, ~ expanded.
initial_workflowsdict[str, WorkflowDefinition]NonePre-parsed workflows, keyed "id@version".
resource_resolverCallable[[str], ResourceDefinition | None]NoneCustom resource lookup.
tool_prefixstr"workflow"Prefix for the 3 registered tools.
tool_name_formatdict[str, str]{}Per-tool name override — keys advance/status/list.
auto_advanceboolTrueLoop elicit → advance automatically until an interactive step is hit.
register_promptsboolTrueAlso register one MCP prompt per workflow.
workflow_resolverWorkflowResolverInMemoryWorkflowResolverSee Extension Points.
session_providerSessionProviderSimpleSessionProviderSee Extension Points.
param_filterParamFilterDefaultParamFilterSee Extension Points.
project_resolverProjectResolverNoneSee Extension Points.
localeLocaleLocale.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).

ProtocolMethod(s)Why implement one
WorkflowResolverasync get_workflow(workflow_id, version=None)
async list_workflows()
Load workflows from a database or remote source instead of a static YAML directory.
SessionProvidersession() -> AsyncContextManager[WorkflowStore]Manage a store's lifecycle per call — e.g. one DB session per request.
ParamFilterfilter_missing_params(wf_def, provided_params)Control which params trigger the elicitation dialog when a workflow starts.
ProjectResolverasync 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.

StoreBackendNotes
InMemoryStorePython dictDefault. Zero dependencies, no persistence across restarts.
SQLAlchemyStoreAsync SQLAlchemy ORMOwn 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" });
OptionTypeDefault
serverMcpServerAdapternew HeadlessAdapter()
storeWorkflowStorenew InMemoryStore()
workflows / agents / resourcesstring[] (YAML)[]
toolPrefixstring"workflow"
toolNameFormatRecord<string,string>{}
localeLocaleEnglish
autoAdvancebooleantrue
registerPromptsbooleantrue
workflowResolverWorkflowResolverInMemoryWorkflowResolver
sessionProviderSessionProviderSimpleSessionProvider
paramFilterParamFilterDefaultParamFilter
projectResolverProjectResolverundefined
resourceResolverResourceResolverundefined

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):

InterfaceMethod(s)Default
WorkflowResolvergetWorkflow(id, version?), listWorkflows()InMemoryWorkflowResolver
SessionProvidersession(): AsyncSession{ store, close() }, manual disposeSimpleSessionProvider
ParamFilterfilterMissingParams(wfDef, providedParams)DefaultParamFilter
ProjectResolverresolveProjectId(projectSlug?)none — project_slug ignored if unset

Storage

StoreBackendNotes
InMemoryStoreJS MapDefault. No persistence.
LocalStorageStoreAny KVStoragenew 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.
No SQL store: unlike the Python SDK's 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

AdapterElicitationUse case
FastMcpAdapterNot supported — ctx.elicit throwsBridges the fastmcp npm package.
OfficialSdkAdapterFull support via server.elicitInputBridges @modelcontextprotocol/sdk's McpServer.
HeadlessAdapterN/A — no transportDefault 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.
LangChainAdapterDeclines (agent-driven fallback)Via createVelaToolkit() — LangChain.js DynamicStructuredTools.
AzureAgentsAdapterDeclines (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.

Don't confuse this with the 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's delegate step, and, in both SDKs, mcp_call and the fetch step 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 invoke on_error and 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 apply auto_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 a projectDataResolver/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.