taskflow runs on OpenCode in two directions, both built
on the host-neutral SubagentRunner seam
(packages/taskflow-core/src/host/runner-types.ts):
- OpenCode as the executor — a taskflow's subagents run as
opencode runsessions (packages/taskflow-hosts/src/opencode-runner.ts). - OpenCode as the caller — taskflow is exposed to an OpenCode user as an
MCP server, so the
taskflow_*tools appear in the session. The MCP protocol, tools, and rendering all live in the host-neutral taskflow-mcp-core package (packages/taskflow-mcp-core/src/mcp/); the opencode adapter just binds them to theopencode runsubagent runner (packages/opencode-taskflow/src/mcp/). This is the direction described here.
The MCP server is dependency-free: it speaks JSON-RPC 2.0 over stdio on Node
built-ins (packages/taskflow-mcp-core/src/mcp/jsonrpc.ts), so taskflow keeps its
zero runtime dependencies guarantee — no @modelcontextprotocol/sdk.
OpenCode has no git-based plugin marketplace, so you register the MCP server directly — either with the CLI:
opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp…or by adding an mcp entry to your project (or global) opencode.json:
The command runs via npx (a version-pinned opencode-taskflow), so the
server is fetched and launched on demand — nothing else to install globally, and
the pin binds the exact code that runs. Verify:
opencode mcp list # → taskflow enabledThe skills.paths entry is optional: OpenCode auto-discovers **/SKILL.md
skills (including Claude Code .claude/skills), and the taskflow tools are
self-describing, so the routing skill just helps OpenCode reach for them at the
right time. A ready-to-copy opencode.json (and the skill) ships in the
package's plugin/ directory.
From a checkout of this repo (no install), point OpenCode at the built bin:
pnpm run build
opencode mcp add taskflow -- node /abs/path/to/taskflow/packages/opencode-taskflow/dist/mcp/bin.jsThe server discovers saved flows and agents from its launch cwd, and each
subagent a flow spawns is itself an opencode run process — no pi process
needed.
OpenCode has no per-run tool-whitelist flag, but it honours a per-process config
injected via the OPENCODE_CONFIG_CONTENT env var. The runner maps each phase's
tool whitelist the same way the codex runner maps to a sandbox mode:
- Read-only phase (no
write/edit/bashin the phase/agenttools) → a permission policy that denies bash/write/edit is injected, so a denied tool call is genuinely rejected (not merely un-approved). This is real enforcement, closer to codex's read-only OS sandbox than to an advisory whitelist. - Mutating phase (or no whitelist) →
opencode run --auto, which auto-approves every permission — the workspace-write analogue. Run flows you trust, ideally in a throwaway worktree (cwd: "worktree").
OpenCode model ids are provider/model (e.g. opencode/deepseek-v4-flash-free,
anthropic/claude-sonnet-4-5) — a clean provider/model passes straight
through to opencode run -m. Because a valid OpenCode id contains a slash, the
runner does not reuse the codex/claude "contains / ⇒ drop" rule; it drops
only ids that clearly aren't OpenCode models: an unresolved role placeholder
({{fast}}), a pi thinking suffix (…:xhigh), or a multi-segment openrouter
path (openrouter/vendor/model, ≥ 2 slashes). A dropped id falls back to
OpenCode's configured default model.
| Tool | What it does |
|---|---|
taskflow_run |
Run a saved flow (name) or an inline define (full DAG or shorthand {task}/{tasks}/{chain}). Returns only the final phase output + a runId. |
taskflow_list |
List saved flows discoverable from the cwd, now with library metadata (purpose, generality, reuseCount) when available. |
taskflow_show |
Show a saved flow as {definition, library} — the library object holds the sidecar metadata (purpose, tags, generality, reuseCount, phaseSignature, …). |
taskflow_save |
Save a flow to the library with optional purpose, tags, and notes. Writes the flow JSON plus a sidecar .meta.json. |
taskflow_search |
Search the library before authoring. Returns ranked reusable flows with score, why, and a reuse hint. Structural + CJK-aware keyword scoring; embedding is Phase 2. |
taskflow_verify |
Statically verify a flow (cycles, missing deps, undefined refs) — no execution. |
taskflow_compile |
Render a flow's DAG as a text outline + a compact status line (with an inline SVG image for clients that render images). |
taskflow_peek |
Inspect one phase's intermediate output from a stored run (post-hoc debugging). Hard-truncated, read-only. |
Inside an OpenCode session, just ask — OpenCode will call the tools:
> List my saved taskflows.
> Verify this flow: {name:"x", phases:[{id:"a",type:"agent",agent:"writer",task:"draft"}]}
> Run the "release-train" taskflow.
Note on approvals. MCP-driven runs are non-interactive, so an
approvalphase auto-rejects (fail-open). Prefer agate(agent review) in flows you run through thetaskflow_*tools; useapprovalonly in flows a human runs interactively.
opencode mcp remove taskflow # or delete the mcp.taskflow entry from opencode.jsonpnpm run test:e2e-opencode-mcp— spawnsbin.tsas OpenCode would and drives the full MCP handshake + tool calls over a real subprocess pipe (no live model needed).pnpm run test:e2e-opencode— runs a 2-phase flow whose subagents are realopencode runsessions (proves OpenCode-as-executor; data flows phase A → B). Uses a freeopencode/model by default (override withPI_TASKFLOW_OPENCODE_MODEL).packages/opencode-taskflow/test/mcp-server.test.ts— protocol + binding tests (in-memory streams).packages/opencode-taskflow/test/opencode-runner.test.ts— opencode JSON parser, model resolution, and permission mapping pinned against real captured events.
{ "$schema": "https://opencode.ai/config.json", "mcp": { "taskflow": { "type": "local", "command": ["npx", "-y", "-p", "opencode-taskflow", "opencode-taskflow-mcp"], "enabled": true } }, "skills": { "paths": ["./node_modules/opencode-taskflow/plugin/skills"] } }