| name | workflow-authoring |
| description | Author runnable TypeScript workflows for OpenCode. Use when creating reusable executable flows under .opencode/workflows or when asked how to write, structure, debug, or invoke a workflow. Triggers on: write a workflow, create workflow, workflow authoring, /workflow, .opencode/workflows. |
| user-invocable | true |
Workflow Authoring
Write executable TypeScript workflows that OpenCode can load and run through /workflow.
Workflows can also call other workflows through ctx.workflow(...).
When To Use This
Use this skill when the user wants a reusable flow with real execution logic instead of a static prompt.
Good fits:
- Repeated multi-step clarifications
- Agent orchestration with stable branching
- Flows that need user questions at controlled points
- Small internal tools that should run inside the current session
Prefer a workflow over markdown commands or pure skills when the behavior needs code, validation, orchestration, or nested reusable steps.
- Use a markdown command for a static prompt template
- Use a skill for reusable instructions or policy
- Use a workflow for executable logic, branching, status updates, questions, subagent delegation, and workflow composition
File Location And Naming
Put workflow files in:
.opencode/workflows/
Rules:
- File extension can be
.ts or .js
- The workflow name is the relative file path without the extension
.opencode/workflows/clarify.ts -> /workflow clarify
.opencode/workflows/release/notes.ts -> /workflow release/notes
Type hints for workflow files are provided by:
.opencode/workflows.d.ts
That file should stay thin and forward to the public workflow interface package:
@lark-opencode/workflow-api
Use that package as the public source of truth for workflow types and helpers. workflows.d.ts should expose it to local workflow files, but the actual interface should live in the shared package so other users and tools can install and read it directly.
That means workflow files can use the global opencode.workflow(...) helper directly, while external readers can inspect the public interface package.
Recommended Imports
Local workflow files
Inside .opencode/workflows/*.ts, prefer the injected global helper for the workflow wrapper itself:
export default opencode.workflow({
run(ctx, input) {
return input.raw
},
})
If you need Zod, import it normally:
import z from "zod"
Public interface imports
If you are documenting, generating, analyzing, or reusing the workflow contract outside the local workflow runtime, import from the public interface package:
import { Args, File, Result, define, type WorkflowContext, type WorkflowDefinition } from "@lark-opencode/workflow-api"
Use the public package when you want a stable source of truth for workflow helpers and types.
Minimal Workflow
Start with the smallest possible shape:
export default opencode.workflow({
description: "Summarize workflow args",
run(ctx, input) {
return {
title: "Args",
output: `name=${ctx.name}\nraw=${input.raw}`,
metadata: { argv: input.argv.length },
}
},
})
Return either:
- a string
- or an object with
title, output, and optional metadata
Runtime Model
When a user runs:
/workflow <name> [args]
OpenCode will:
- Resolve the workflow by file name
- Create a normal user message in the current session
- Run the workflow
- Save the workflow result back into the session as an assistant response
If one workflow file is broken, OpenCode should not crash. That workflow becomes unavailable and /workflow <name> reports its load error.
The run(ctx, input) API
Every workflow gets a context object and parsed input.
ctx.status(...)
Use this to expose progress while the workflow is running.
Prefer ctx.status({ progress }) and store workflow progress under workflow_progress using the public workflow-progress.v2 shape.
await ctx.status({
title: "Comparing options",
progress: {
version: "workflow-progress.v2",
workflow: { status: "running", label: "Clarify workflow" },
machine: {},
step_definitions: [],
step_runs: [],
transitions: [],
participants: [],
},
})
Use it early and before expensive work. Keep the payload small and deterministic.
Built-in examples: implement already tracks its main flow with full v2 state-machine progress, and plan now emits v2 progress for init, PM, review, judge, notify, decide, done, and failed stages.
ctx.ask(...)
Use this when the user must choose something.
const reply = await ctx.ask({
questions: [
{
header: "Mode",
question: "Which mode should we use?",
options: [
{ label: "Fast", description: "Quick pass" },
{ label: "Deep", description: "Thorough pass" },
],
},
],
})
const picked = reply.answers[0]?.[0] ?? "Fast"
return `Selected: ${picked}`
ctx.task(...)
Use this to delegate a focused subtask to another agent.
const out = await ctx.task({
description: "PM review",
prompt: "Analyze these requirements and propose 2-3 options",
subagent: "metis",
category: "deep",
})
return out.text
Available fields:
description: short task label
prompt: subagent prompt
subagent: agent name
category?: model routing category
model?: explicit model override
session_id?: continue an earlier subagent session
load_skills?: inject one or more skills into the subagent prompt
ctx.workflow(...)
Use this to call another already-defined workflow.
const out = await ctx.workflow({
name: "child",
raw: input.raw,
})
return `child said: ${out.output ?? ""}`
Available fields:
name: workflow name, matching its path under .opencode/workflows
raw?: raw argument string for the child workflow
argv?: explicit split args if you do not want raw parsing
files?: optional file attachments for the child workflow
Use ctx.workflow() when you want to reuse another workflow's executable behavior instead of copying the same logic into multiple files.
Context Fields
Useful fields already available on ctx:
ctx.name: workflow name
ctx.raw: raw argument string after the workflow name
ctx.argv: split args after the workflow name
ctx.files: attached files from the current message
ctx.sessionID: current session id
ctx.directory: current working directory
ctx.worktree: repo root
Input Parsing Patterns
Default Input
If you do not define a custom schema, input is:
{
raw: string
argv: string[]
files: WorkflowFile[]
}
This is enough for many workflows.
export default opencode.workflow({
run(ctx, input) {
const target = input.argv[0] ?? "."
return `Inspecting ${target}`
},
})
Custom Schema
If you want stricter parsing, define input with a Zod schema.
If you only need the existing fields, reuse opencode.args:
export default opencode.workflow({
input: opencode.args,
run(ctx, input) {
return input.raw || "No args"
},
})
If you need derived fields, use a Zod transform. Example:
import z from "zod"
export default opencode.workflow({
input: opencode.args.transform((input) => ({
...input,
dry: input.argv.includes("--dry"),
target: input.argv.find((x) => !x.startsWith("--")) ?? ".",
})),
run(ctx, input) {
return `${input.dry ? "Dry run" : "Run"}: ${input.target}`
},
})
Important:
- The runtime passes
{ raw, argv, files } into your schema
- If you want extra fields, derive them from those values
- Keep schemas simple; avoid overengineering command parsing
Example: Clarify With Multiple Agents
export default opencode.workflow({
description: "Compare requirement options before asking the user",
async run(ctx, input) {
await ctx.status({
title: "Running internal review",
progress: {
version: "workflow-progress.v2",
workflow: { status: "running", label: "Clarify workflow" },
machine: {},
step_definitions: [],
step_runs: [],
transitions: [],
participants: [],
},
})
const pm = await ctx.task({
description: "PM review",
prompt: `Analyze this requirement and propose product directions:\n\n${input.raw}`,
subagent: "metis",
category: "deep",
})
const qa = await ctx.task({
description: "QA review",
prompt: `Find risks, edge cases, and rollout concerns:\n\n${input.raw}`,
subagent: "momus",
category: "deep",
})
const ans = await ctx.ask({
questions: [
{
header: "Direction",
question: "Which direction should we take?",
options: [
{ label: "PM-first", description: "Favor product value and speed" },
{ label: "QA-first", description: "Favor safety and predictability" },
],
},
],
})
return {
title: "Clarify result",
output: [`PM:\n${pm.text}`, `QA:\n${qa.text}`, `Chosen: ${ans.answers[0]?.[0] ?? "none"}`].join("\n\n"),
}
},
})
Example: Nested Workflow
export default opencode.workflow({
async run(ctx, input) {
const out = await ctx.workflow({
name: "child",
raw: input.raw,
})
return {
title: "Parent",
output: `parent:${out.output}`,
}
},
})
This is useful when a larger workflow wants to reuse a smaller one for parsing, formatting, triage, or standard decision logic.
How To Run A Workflow
From the TUI:
/workflow clarify should the feature default to auto-save?
Examples:
/workflow release/notes v1.2.0
/workflow clarify add approval flow for production deploys
If the workflow accepts file attachments, attach them in the current prompt before running /workflow ....
Third-Party Reuse
If another repo, tool, or documentation generator wants to understand the workflow contract, it should install and read:
@lark-opencode/workflow-api
That package is the public interface library for:
- default workflow args
- workflow result shape
- workflow context methods
- nested workflow invocation
- subagent task invocation
Recommended reuse patterns:
- Import its exported types to build editor support, docs, or validation helpers
- Treat
.opencode/workflows.d.ts as the local bridge, not the canonical interface definition
- If you need to mirror the contract elsewhere, mirror
@lark-opencode/workflow-api, not a copied ad hoc type block
For local authoring, keep using opencode.workflow(...). For public integrations and references, point people at @lark-opencode/workflow-api.
Debugging And Failure Handling
If a workflow is not being found:
- Check the file path under
.opencode/workflows
- Check the invoked name matches the relative path without
.ts
- Check that the file exports
default
If a workflow fails to load:
- Look for top-level throws
- Check syntax errors and bad imports
- Keep top-level code minimal; put logic inside
run() when possible
If nested workflow calls fail:
- Check the child workflow name matches its relative path
- Check for workflow cycles like
a -> a or a -> b -> a
- Keep nesting shallow and purposeful
- Prefer shared TS helpers when the logic is not really a workflow step
If a workflow runs but returns bad output:
- Return a string or
{ title?, output?, metadata? }
- Use
ctx.status({ progress }) to narrow down where it fails
- Keep
metadata small and serializable
If custom parsing behaves unexpectedly:
- Remember your schema receives only
{ raw, argv, files }
- Derive extra fields from those values with Zod transforms or inside
run()
Authoring Rules
When writing a workflow for the user:
- Keep the file in
.opencode/workflows/
- Use
export default opencode.workflow({ ... })
- Prefer the default
{ raw, argv, files } input unless stricter parsing is clearly useful
- Use
ctx.ask() only for real user decisions
- Use
ctx.task() for focused subagent jobs, not broad open-ended delegation
- Use
ctx.workflow() when another workflow already models the step cleanly
- Keep top-level code safe so load failures stay isolated
- Avoid workflow cycles and unnecessary nesting
- Return short, readable output that works well inside a session transcript
- Prefer
workflow-progress.v2 progress snapshots when the workflow has meaningful phases or waits
Quick Checklist