Counting visitors…
Browse all docs
Start · 9Find your path through RunifoldLearn Runifold in 45 minutesUnderstand the complete Runifold platformYour first trustworthy runChoose the right execution APIChoose crates and Cargo featuresBuild common Runifold applicationsRunifold frequently asked questionsTroubleshoot Runifold applications
Execution kernel · 7Understand RunContextCoordinate external effects safelyBound work with budgets and cancellationHandle errors and retries safelyEvents, journals, and execution evidenceDesign capability-safe executionRecover safely from checkpoints
Models & providers · 7Route across models without duplicate outputChoose and configure a providerUse the provider-neutral model protocolBuild on the Provider Runtime contractUse OpenAI control-plane and Realtime APIsTest and benchmark provider adaptersSet up OpenAI, Anthropic, Gemini, and Ollama
Agents · 7Build and configure an AgentGive an Agent typed toolsAdd conversations and semantic memoryDelegate to child Agents safelyReturn structured Rust valuesStream without losing semanticsGround an Agent with retrieval
Durable workflows · 7Compose deterministic workflowsMake workflows durableOperate durable workflow workersCoordinate timers, signals, and durable waitsRun multi-tenant workflow infrastructureRun parallel branches and safe racesVersion and evolve durable workflows
Integrations · 7Connect through MCPChoose stores and persistence boundariesExpose durable work through MCP TasksBuild and evaluate retrieval pipelinesUse MCP Resources, Prompts, and SamplingCache MCP responses without crossing authorityDeploy Runifold in a Rust web service
Quality & operations · 10Test without the networkEvaluate quality and prevent regressionsObserve the complete run treeRun safely in browsers and at the edgeRead reliability claims preciselyRun reproducible evaluations in CIOperate Runifold with SLOsGovern Task retention and deletionArchive audit evidence to S3-compatible WORM storageManage compatibility and trusted releases
Docs/Integrations
NEW TO RUNIFOLD?Build the complete mental model in 45 minutes
Integrations

Expose durable work through MCP Tasks

Map workflow tasks to MCP handles with polling, updates, cancellation, subscriptions, retention, and tenant-safe authorization.

Practical guide·18 min

The Task protocol

Ordinary MCP tool calls complete within one request. MCP Tasks represent work that outlives that request. A client creates a Task and receives an opaque taskId plus current status, polling guidance, retention timestamps, and eventual result or safe failure detail.

Tasks require negotiated capability and per-request authorization. A server that does not support the extension continues to use normal MCP behavior.

Enable the extension

The Workflow adapter is an opt-in feature on runifold-mcp:

Cargo.toml
[dependencies]
runifold = { version = "=0.9.0", features = ["mcp", "sqlite-bundled"] }
runifold-mcp = { version = "=0.9.0", features = ["workflow-tasks"] }

On the client, advertise Task support during initialization:

let config = McpClientConfig::new(Implementation::new("task-client", "1"))
    .with_tasks();
let client = McpClient::new(transport, config);
let initialized = client.initialize().await?;
println!("task capability: {:?}", initialized.capabilities.tasks);

If the peer does not negotiate Tasks, call the Tool through the ordinary request/response path or disable long-running mode. Do not assume every Tool call may become a Task.

Workflow mapping

With the workflow-tasks feature, WorkflowTaskAdapter binds one MCP Tool route to an exact Workflow name, version, and tenant. Queued, leased, Timer, and Signal waits map to working; durable interrupts map to input_required; terminal checkpoints map to completed, failed, or cancelled.

Results are reconstructed from immutable terminal history. Route identity must be stable so a Task ID can recover the same contract after server restart.

Client lifecycle

Clients can explicitly:

  • read current state with get_task;
  • submit keyed input with update_task;
  • cooperatively cancel with cancel_task;
  • wait under one operation deadline with wait_task;
  • receive typed snapshots with listen_tasks.

Polling intervals are bounded by client policy, remaining deadline, and retention. A completed Tool result with isError: true is still a completed Task; protocol failure is a different layer.

Call a Tool and handle both outcomes

use runifold::mcp::{CallToolOutcome, CallToolParams};
 
let outcome = client
    .call_tool_outcome(CallToolParams {
        name: "durable_report".into(),
        arguments: Some(serde_json::Map::from_iter([
            ("report_id".into(), serde_json::json!("rpt_42")),
        ])),
    })
    .await?;
 
let result = match outcome {
    CallToolOutcome::Complete(result) => result,
    CallToolOutcome::Task(task) => {
        println!("task={} status={:?}", task.task_id, task.status);
        client.wait_task(task).await?
    }
};
 
if result.is_error {
    eprintln!("Tool completed with an application error: {:?}", result.content);
} else {
    println!("structured={:?}", result.structured_content);
}

wait_task polls according to the server recommendation while honoring the client's operation deadline. Persist the Task ID in application state if the UI or process may disconnect; after reconnect, call get_task and then wait_task again. The durable Store—not an in-memory subscription—is the source of truth.

Subscriptions

Task subscriptions emit an immediate durable snapshot, then only changed states, and detach after a terminal snapshot. Reconnection reads current Store state instead of depending on process-local notification replay.

Bound Task IDs per subscription and refresh cadence. Transient Store failures must not invent transitions. Authorization and tenant isolation are re-evaluated whenever state is derived.

Security and retention

Task IDs are identifiers, not bearer credentials. Every lookup stays scoped to the adapter's tenant. Cross-tenant mismatch should not disclose existence. Task input has the same trust level as ordinary MCP elicitation or sampling.

Protocol ttlMs describes handle usability from creation. It is not a Workflow deadline and does not physically delete active work. Terminal cleanup is a separate fenced control plane with immutable tombstones.

Bind a Tool route to a Workflow

Construct WorkflowTaskAdapter with the same durable WorkflowStore used by workers, then register a stable mapping:

let mut tasks = WorkflowTaskAdapter::new(store.clone());
tasks.register_route(WorkflowTaskRoute::new(
    "durable_report",      // MCP Tool name
    "generate-report",     // Workflow name
    1,                     // exact Workflow version
    WorkflowTenantId::parse("tenant-acme")?,
)?)?;
 
let server = server.with_task_backend(Arc::new(tasks));

The MCP Tool must also be registered and capability-granted on the server. A separate Workflow worker must register generate-report version 1 and process the shared Store. Restarting the MCP transport does not cancel or erase the Task.

Task troubleshooting

SymptomCheck
call always completes synchronouslyTask capability, route registration, and Tool name match
DefinitionUnavailableworker registry contains the exact Workflow name/version
Task remains workingqueue claim, tenant budget, timer, lease, and worker health
Task is input_requireddisplay keyed requests and submit each key once with update_task
reconnect loses progressuse the same durable Store and persisted Task ID
cross-tenant lookup reveals existencebind adapter route and every lookup to authenticated tenant
completed result has is_errortreat it as application completion, not protocol failure