MCP tools reference

Every tool exposed to Claude Code, with input and output shapes.

The Roscoe MCP server (started by roscoe mcp) exposes 32 tools to Claude Code over stdio. They're grouped here by purpose. Tool names and input schemas are the canonical surface: anything not listed here is not part of the MCP contract.

All tools return JSON wrapped in MCP's content envelope. Error shapes are not uniform across groups:

  • Run tools set isError only for genuine server failures and return { error } for user errors.
  • Workflow-writer tools always set isError: true and return { error, code }, where code is a WriterErrorCode.
  • test_workflow returns { ok: false, code, message }.

Treat code as the thing to branch on where a tool documents one, and don't assume ok: false is present. The grouping below mirrors apps/mcp/src/tools/*.ts.

Project scoping (cwd / global)#

Every tool below except the Docs group takes a scope input — exactly one of:

Input Type Notes
cwd string Absolute path to any directory inside the project this call applies to — typically the user's IDE working directory. Resolved to the git root.
global boolean Set true to operate on the global ~/.roscoe store instead of a project. Use when there is no project directory (e.g. the Claude desktop app).

The MCP server is long-lived and project-agnostic; the scope tells each call where it applies. With cwd, the server resolves it to its enclosing git root, derives the project id, and isolates workflows + runs to that project. Two parallel calls with different cwds see different projects (per-call AsyncLocalStorage isolation). With global: true, there is no project: tools resolve from the global store, and runs are tracked in a shared global namespace. Workflow create/move tools may simply OMIT source — it defaults to repo when a repo source exists and global otherwise — so an explicit source: "global" is optional in global mode, while source: "repo" there returns no_repo_source.

Passing neither errors with CWD_REQUIRED; passing both errors with CWD_GLOBAL_CONFLICT. The server never silently guesses a project.

For brevity, the per-tool input tables below omit the scope row. Assume every project-scoped tool requires cwd or global.

Validation errors (returned as structured { error, code, cwd? }):

Code Meaning
CWD_REQUIRED neither cwd nor global was passed
CWD_GLOBAL_CONFLICT both cwd and global were passed
NOT_ABSOLUTE cwd was empty or relative
NOT_FOUND cwd does not exist
NOT_DIR cwd is a regular file, not a directory
PROTECTED_PATH cwd resolves into a protected system prefix (/etc, /usr, /System, …)
NOT_GIT_REPO cwd is not inside a git working tree
PROJECT_ID_UNAVAILABLE git remote URL and repo basename both unavailable

Cross-project access. Every run-lookup tool — get_run_status, advance_run, advance_consensus, advance_round_robin, advance_map, resume_run, cancel_run, get_run_history — compares the run's projectId to the caller's. A mismatch returns "Run not found." rather than leaking that the run exists in another project. Scope ownership is strict and symmetric: global-namespace runs (projectId = '', started with global: true) are reachable only by a global caller, and a project's runs only by that project's caller. This matches list_runs, which filters by the caller's exact project, so the by-id guard and the listing always agree.

The Docs tools (list_docs, get_doc, search_docs, ask_docs, answer_docs) are project-agnostic and do not take cwd.

Index#

Group Tools
Workflow CRUD list_workflows, get_workflow, create_workflow, update_workflow, delete_workflow, list_workflow_references, move_workflow, duplicate_workflow, rename_workflow
Authoring helpers get_workflow_schema, validate_workflow, lint_workflow, audit_workflow
Run lifecycle start_workflow, get_run_status, advance_run, advance_consensus, advance_round_robin, advance_map, resume_run, cancel_run
Run observability list_runs, get_run_history
Verification test_workflow
Skill import list_skills, convert_skill, finalize_skill_conversion
Docs list_docs, get_doc, search_docs, ask_docs, answer_docs

Workflow CRUD#

list_workflows#

Lists all workflows from both Global and Project sources.

Input Type Required

Returns: Array<{ id, name, description: string | null, source: 'global' | 'repo', shadowed: boolean }>

When the same id exists in both sources, repo wins and the global entry has shadowed: true. Cached agent prompts that assume single-dir lookups will silently start picking up repo workflows once a repo source exists — pass source: "global" to disambiguate.

get_workflow#

Returns the full workflow configuration.

Input Type Required
workflow_id string yes
source 'global' | 'repo' no

Returns: the parsed WorkflowConfig plus a source field. When source is omitted, resolves repo-first if a project source exists.

create_workflow#

Writes a new workflow YAML file.

Input Type Required Notes
id string yes Must match [a-zA-Z0-9_-]+
source 'global' | 'repo' no Defaults to repo when a project source exists, else global
config object no If omitted, writes a minimal template
overwrite boolean no Default false. Refuses on collision when false.

Returns: { id, source, path } on success, or { error, code, details? }. Validates schema + integrity before writing. code values: invalid_id, invalid_config, integrity_error, conflict, no_repo_source, io_error.

update_workflow#

Replaces an existing workflow's contents. Validates schema + integrity; refuses to write on either failure.

Input Type Required
id string yes
source 'global' | 'repo' no
config object (WorkflowConfig) yes

Returns: { ok: true, source, path } or { error, code, details? }.

delete_workflow#

Hard-deletes a workflow file. Requires literal confirm: true to actually delete, and REFUSES when another workflow references this one from a subworkflow node — returning { error, referencedBy } so you can see what would break. Pass force: true to delete anyway.

Input Type Required Notes
id string yes
source 'global' | 'repo' no
confirm true (literal) yes
force boolean no Default false. Delete even if referenced.

Returns: { ok: true, path, source }, { error, referencedBy } when referenced, or { error, code }.

list_workflow_references#

List workflows that reference the given workflow through a subworkflow node: the parents that would be orphaned by a delete, or left pointing at a dead id by a rename without a cascade. Call before delete_workflow or rename_workflow. The search covers every workflow on disk regardless of source, so source scopes nothing here; it exists only for input-shape consistency with the rest of this group.

Input Type Required
workflow_id string yes
source 'global' | 'repo' no

Returns: { referrers: Array<{ workflowId, nodeId }> } — one entry per referencing subworkflow node.

move_workflow#

Move a workflow between Global and Project. Refuses if the target source already has the same id.

Input Type Required
id string yes
to 'global' | 'repo' yes

Returns: { ok: true, id, from, to, path } or { error, code }.

duplicate_workflow#

Copy a workflow to a new id (and optionally a new source). Updates the new file's name field to match to_id.

Input Type Required
from_id string yes
from_source 'global' | 'repo' no
to_id string yes
to_source 'global' | 'repo' yes
overwrite boolean no

Returns: { id, source, path } or { error, code, details? }.

rename_workflow#

Rename the file stem AND update the name field on disk. Refuses if new_id already exists in the same source.

By default the rename cascades: every workflow whose subworkflow node points at the old id is rewritten to the new one. Pass rewrite_references: false to rename the file alone and leave those references pointing at an id that no longer resolves.

Input Type Required Notes
id string yes
new_id string yes
source 'global' | 'repo' no
rewrite_references boolean no Default true — cascade the rename.

Returns: { id, source, path, rewritten, failed, rolledBack } or { error, code, details? }. rewritten lists the workflows updated, failed those that could not be, and rolledBack is true when a partial failure caused the whole rename to be undone.

Authoring helpers#

get_workflow_schema#

Returns the JSON Schema for the workflow YAML, the registered executor list (with their config keys), the model ids permitted by roscoe.yaml, and an authoring array of runtime gotchas worth reading before you write. Call this once before authoring a new workflow so you have the exact shapes your write will be validated against.

Input Type Required

Returns: { jsonSchema, executors: ExecutorSummary[], models: { available, default } | { error } }.

validate_workflow#

Dry-run schema + integrity validation. No filesystem writes. Pass either the full config OR id (+ optional source).

Input Type Required (mutex)
config object one of config or id
id string one of config or id
source 'global' | 'repo' no

Returns: { valid: true, errors: [] } or { valid: false, code, errors: string[] }.

lint_workflow#

Schema + integrity validation plus a non-fatal lint pass: dead states, unreachable transitions, models missing from roscoe.yaml. Same input shape as validate_workflow.

Input Type Required (mutex)
config object one of config or id
id string one of config or id
source 'global' | 'repo' no

Returns: { errors: string[], warnings: { code, message, state? }[] }. Warning codes include dead_state, unreachable_state, unknown_model.

Run lifecycle#

start_workflow#

Start a workflow in MCP mode. Synchronously runs through any script / condition nodes, then pauses at the first AI or human node and returns a pendingStep.

Input Type Required Notes
workflow_id string yes
context record no
source 'global' | 'repo' no
host 'claude_code' | 'cowork' | 'desktop_chat' no Your surface. Decides whether fan-out can run on the user's subscription.
spending_cap_usd number (≥ 0) no Run stops with stop_reason: "spending_cap_exceeded" once the total exceeds it.
spending_cap_tokens integer (≥ 0) no Same, on total input + output tokens.

Returns: { runId, status, url, workflow, narration, pendingStep? } plus the AdvanceResult cost fields, and hostHint when a metered desktop-chat caller hits a fan-out node. workflow is { id, name, description } and narration is a ready-to-send opening line naming the workflow and its live URL. pendingStep appears for any node that hands back — ai_agent, ai_judge, consensus, human, round_robin, map. url points at the run page on the active server (dev port or roscoe serve port) and is a deep link: it carries a ?project= query (?project=global for a global: true run, otherwise the run's projectId) so opening it auto-selects the matching workspace in the web UI. runId is alphanumeric-only so the URL survives markdown rendering in a chat client unescaped.

get_run_status#

Get the current status of a run, including any pending AI step.

Input Type Required
run_id string yes

Returns: the same { runId, status, url, pendingStep? } shape.

Important: if the returned status is paused and the pendingStep is for a human node, you must NOT call resume_run autonomously — display the prompt and validTransitions to the user as a numbered list and wait for their reply. Only ai_agent / ai_judge pending steps may be answered programmatically (via advance_run).

advance_run#

Provide Claude's response to a pending AI step. For ai_judge, the response must satisfy the configured validator (the JSON envelope shape the prompt asked for). For ai_agent, any response is accepted.

Input Type Required Notes
run_id string yes
response string yes
usage object no Real token counts / cost: { inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, costUsd }, all optional. Supplying it records the exact API-equivalent cost instead of estimating from text length.
thinking string no The model's chain-of-thought for this step. Stored on the turn and shown in the run inspector's Exchange tab.

Returns: the updated AdvanceResult shape (status, optional pendingStep for the next pause).

advance_consensus#

Advance a consensus step that paused for a subagent-capable host (Claude Code / Cowork): spawn agentCount independent subagents (none sees another's answer), each on the pendingStep model and prompt (or, if the pendingStep carries an agents array, subagent i runs agents[i].prompt on agents[i].model), and return every raw response here.

A most-consistent (USC) node pauses twice, signalled by consensusPhase: generate (each subagent returns free-form prose, not a JSON envelope), then aggregate (one subagent picks the most consistent response and returns {"reasoning": "...", "index": N}). For gate/answer nodes Roscoe validates each verdict; if some fail and a fix could still change the outcome, the node re-pauses with consensusPhase: 'retry' and a reduced agents[] covering only the failed ones.

Input Type Required Notes
run_id string yes
results Array<{ raw: string }> yes (min 1) One entry per subagent, in agents[] order when that array was given.

Returns: the updated AdvanceResult shape.

advance_round_robin#

Advance a round_robin step, keyed by the pendingStep's roundRobinPhase. When it's resolve (candidates need a model call), the pendingStep carries a single agents[0].prompt: run one subagent and return its text as result. Otherwise (first / contested) the pendingStep carries criterion, model, and matchups (each { id, a, b }): spawn one independent subagent per matchup, have it pick A or B (no ties) with a 0-100 decisiveness margin, and return verdicts keyed by matchup id. Submit verdicts as waves complete; the node re-pauses with any still-outstanding matchups (and, in contested mode, a reversed re-judge batch) until every matchup is in.

Input Type Required Notes
run_id string yes
verdicts Array<{ id, winner: string, margin: number, reasoning?: string }> no Matchup rounds only (first/contested).
result { raw: string } no Resolve round only (roundRobinPhase: 'resolve').

Returns: the updated AdvanceResult shape.

advance_map#

Advance a map step. The pendingStep carries agents (one { model, prompt } per subagent), agentCount, and mapPhase. Run exactly agentCount independent subagents on agents[i].model / agents[i].prompt and return each raw response here, in agent order, regardless of phase: extract (one subagent turns a free-text list into branch prompts, then the node re-pauses with them), map (one subagent per branch; an empty response is a failed branch), or reduce (one subagent synthesizes the branch outputs). After the map round, a configured reduce re-pauses the node once more if at least one branch succeeded.

Input Type Required Notes
run_id string yes
results Array<{ raw: string }> yes (min 1) One entry per subagent, in agent order: agentCount on the map round, exactly 1 on the reduce round.

Returns: the updated AdvanceResult shape.

resume_run#

Resume a workflow paused at a human node by supplying the chosen transition.

Input Type Required
run_id string yes
transition string yes

Do not call autonomously. A human node represents a required human decision point. When a run is paused at one, you must (1) display the node's prompt, (2) present the valid transitions as a numbered list, and (3) wait for the user to reply with their choice before calling this tool. Do not infer or assume the choice.

Returns: the updated run detail.

cancel_run#

Cancel a running or paused workflow. In-flight executor work (a running script worker or claude CLI/SDK call) is aborted so it stops promptly rather than finishing in the background, and the node the run was sitting on is finalized with the dedicated cancelled status (distinct from failed). Errors (not a no-op) if the run is already terminal.

Input Type Required
run_id string yes

Returns: the run detail.

Run observability#

list_runs#

List recent workflow runs, newest first.

Input Type Required Notes
workflow_id string no
status pending|running|paused|completed|failed|cancelled no
since ISO 8601 datetime no Returns runs with startedAt >= since
limit int 1..500 no Default 25
kind 'real' | 'test' | 'all' no Default 'real'. test shows ephemeral test rows

Returns: an array of { id, workflowId, workflowName, status, startedAt, completedAt, source, workflowSource, kind }.

get_run_history#

Full per-node trace for one run, including child sub-runs (e.g. consensus agents).

Input Type Required
run_id string yes

Returns:

{
  run: {
    id, workflowId, workflowName, status,
    startedAt, completedAt, source, workflowSource, context,
    parentRunId,        // the parent run, when this is a subworkflow child
    childRunIds,        // subworkflow runs this run spawned
    awaitingChild       // { childRunId } while paused on a subworkflow node
  },
  nodes: Array<{
    id, nodeId, nodeType, status,
    startedAt, completedAt, durationMs, attempts,
    input, output, error,
    children: Array<{ id, nodeId, nodeType, status, ... }>
  }>
}

Verification#

test_workflow#

Execute a workflow end-to-end against the real runner and the real DB, then delete the run row in a finally block. Identical execution path to production runs; the only differentiator is kind: 'test' on the workflow_runs row, which keeps these out of user-facing run lists.

Input Type Required (mutex) Notes
workflow_id string one of workflow_id or config
source 'global' | 'repo' no
config object one of workflow_id or config Inline WorkflowConfig
context record no Same shape as start_workflow.context
mock_responses Record<nodeId, string> no Required for any AI/human node reached
timeout_ms int 100..600000 no Default 60000

Returns (success):

{
  ok: true,
  finalStatus: 'completed' | 'failed' | 'cancelled',
  durationMs: number,
  nodeTrace: Array<{ nodeId, nodeType, status, input, output, error, children, ... }>
}

Returns (failure): { ok: false, code, message, nodeId?, nodeType?, finalStatus?, durationMs?, nodeTrace?, details? }. code values: invalid_input, workflow_not_found, invalid_config, missing_mock, timeout, runner_error, fanout_in_test_mode_unsupported, unexpected.

Limitations and warnings:

  • Every fan-out nodeconsensus, round_robin, map, and subworkflow (whose child may itself fan out) — calls the live Anthropic API directly and cannot be mocked. Workflows containing one are rejected with fanout_in_test_mode_unsupported unless ANTHROPIC_API_KEY is set, in which case real API spend will occur. The classification comes from the shared findFanoutNode, so a new fan-out node type is covered here and in start_workflow's gate from one definition — do not re-list the types by hand, which is how round_robin was once missed.
  • The same rejection fires when the workflow config cannot be read.
  • timeout_ms cannot interrupt a single non-yielding script — set the script's own timeout for that workload.
  • The runner sweeps orphaned kind: 'test' rows on MCP startup (gated on ROSCOE_SWEEP_TEST_RUNS=1, set automatically by the MCP entry point).
  • Mocks are required for any ai_agent, ai_judge, or human node the run reaches. Missing mocks fail fast with code: 'missing_mock' rather than hanging.

Skill import#

Convert markdown skills (the Agent Skills open standard — SKILL.md files with name + description frontmatter, optionally bundled with helper scripts) into Roscoe workflows. Works across Claude Code, Codex CLI, OpenCode, Gemini CLI, Cursor, and other harnesses that follow the standard.

list_skills#

Find skill candidates by content signature across the standard skill roots plus the current repo tree. A skill is any .md file whose YAML frontmatter contains both name: and description: (and is not a Jekyll/Hugo blog post).

Input Type Required Notes
content_scan boolean no Default true. Set false to check only the standard harness roots and skip the repo-tree fallback.

Scan roots checked (in order):

  • User-global: ~/.claude/skills/, ~/.agents/skills/, ~/.codex/skills/ (honors $CODEX_HOME), ~/.config/opencode/skills/, ~/.gemini/skills/
  • Project-local (walks up to git root): .claude/skills/, .agents/skills/, .opencode/skills/
  • Fallback: the repo tree, walked to a depth of 6. It does not consult .gitignore — the only exclusions are a fixed set: .git, node_modules, dist, build, .next, .turbo, .nx, coverage, .cache. Skipped entirely when content_scan: false.

Returns: { count, skills: Array<{ path, name, description }> }.

convert_skill + finalize_skill_conversion#

Convert one skill into a Roscoe workflow, in two steps so the conversion runs on your Claude subscription (the host runs the completion) rather than the Roscoe server calling a model. Bundled scripts/ are copied alongside the workflow and reachable from script nodes via $ROSCOE_WORKFLOW_DIR. Out of scope: references/, examples/, assets/ folders are detected and reported but not bundled. For a worked walkthrough, see Import a workflow from a skill.

Step 1 — convert_skill resolves the skill, reads its bundle, and returns a converter prompt for you to run as your own completion. It makes no server-side model call.

Input Type Required Notes
skill_path string yes Absolute path to a skill directory or markdown file.
target_id string no Workflow id. Defaults to the skill's name frontmatter, slug-normalized.
target_source 'global' | 'repo' no Defaults to repo when a project source exists, otherwise global. Pass 'global' explicitly to force the user-global location.
overwrite boolean no Default false. Refuses on id collision otherwise.

Returns a handback: { action: 'run_completion', prompt, model, finalize_args }. Run prompt yourself, then pass the raw output plus finalize_args to finalize_skill_conversion. (Resolve failures return { ok: false, stage: 'resolve', message }.)

Step 2 — finalize_skill_conversion takes your completion and writes the workflow. It re-reads the skill bundle from skill_path, so nothing you echo back is trusted as a write source.

Input Type Required Notes
raw_model_output string yes The completion you ran on convert_skill's prompt.
skill_path string yes From finalize_args (the bundle is re-read from it).
target_id string no From finalize_args.
target_source 'global' | 'repo' yes From finalize_args.
overwrite boolean no From finalize_args. Default false.

Returns: { id, source, path, assetCount, skippedBundleDirs, skippedNote? }. Failure shape: { ok: false, stage, message } where stage is one of resolve, parse, validate, write. On parse/validate, the response includes rawModelOutput so you can re-run the prompt or repair — do not fabricate a workflow to force success.

Conversion guidance the prompt gives the model:

  • Use {{ nodeId.field }} template interpolation whenever a step references "the previous result", "the gathered context", etc.
  • A reference is only valid if the producing node runs on every path that reaches the consuming node (dominance check). Branching workflows must reference a node both branches converge through, or use a fallback like {{ nodeId.field || "default" }}.
  • Default to ai_judge for routine routing; reach for consensus (3-5 agents with a quorum) for high-stakes decisions (production deploys, security severity, migration safety).
  • Cycles (verify → fix → re-verify) must have maxIterations on at least one participating node, unless a human node sits in the loop.

Docs#

The docs tools cover three retrieval modes plus a synthesized Q&A wrapper. Pick by intent:

Tool Use when
list_docs You need the catalog — slugs and titles for every page.
get_doc You already know the slug and want the entire page.
search_docs Shallow lookup — ranked snippets are enough.
ask_docs You want full heading sections to ground your own answer (preferred for LLM hosts).
answer_docs You want the retrieval packaged into a citation-required prompt to answer yourself.

list_docs#

Returns the flat manifest [{ slug, title, description }] for every doc page on disk.

Input Type Required

get_doc#

Fetch the full markdown body for one slug.

Input Type Required Notes
slug string yes No leading slash, no .md suffix (e.g. recipes/approval-flow)

Returns: { frontmatter, markdown } or { error }.

search_docs#

Full-text search across all docs.

Input Type Required Notes
query string yes Supports prefix + fuzzy
limit int 1..50 no Default 20

Returns: ranked hits, each with a headingId and a snippet of up to ~80 characters either side of the match — call get_doc (or ask_docs for richer context) for the full body.

ask_docs#

Knowledge-base retrieval. Takes a natural-language question and returns the most relevant heading sections (the markdown from a heading down to the next equal-or-higher heading), packed within a character budget. Designed for LLM hosts that want enough grounded context to answer the question themselves.

Input Type Required Notes
question string yes Natural-language question
maxSections int 1..20 no Default 5
maxChars int 500..20000 no Total char budget. Default 6000.

Returns:

{
  question: string;
  sections: Array<{
    slug: string;
    title: string;
    headingId: string | null; // null = page lede
    headingText: string | null;
    text: string; // full section markdown (mermaid + code preserved)
    score: number;
  }>;
  truncated: boolean; // true if more candidates were dropped to fit budget
  totalChars: number;
}

The headingId matches the id rehype-slug produces in the rendered DOM, so /docs/${slug}#${headingId} is a valid deep link.

answer_docs#

Q&A packaging. Internally calls ask_docs, then returns a citation-required prompt for you to answer yourself — it makes no server-side model call, so the answer runs on your Claude subscription. Present your answer with the returned citations. LLM hosts that just want the raw source should prefer ask_docs.

Input Type Required Notes
question string yes
model string no Suggested model id for the answer. Default claude-haiku-4-5.
maxSections int 1..20 no Default 5. Forwarded to ask_docs.
maxChars int 500..20000 no Default 6000. Forwarded to ask_docs.

Returns:

{
  action: 'answer_prompt';
  instructions: string; // answer `prompt` yourself, cite `citations`
  prompt: string; // the citation-required answer prompt to run
  model: string; // suggested model id
  citations: Array<{
    slug: string;
    title: string;
    headingId: string | null;
    headingText: string | null;
  }>;
}

Failure modes:

  • Empty/whitespace question → error.
  • Zero relevant sections in retrieval → error (the tool refuses to hand back a prompt with no source; fall back to ask_docs to inspect the empty retrieval).

AdvanceResult shape#

The shared shape returned by start_workflow, get_run_status, advance_run, advance_consensus, advance_round_robin, advance_map, resume_run, and cancel_run:

{
  runId: string;
  status: 'running' | 'completed' | 'failed' | 'paused' | 'cancelled';
  url: string;          // deep link: /open/runs/<id>?project=<projectId|global>
                        // — /open hands the run to the desktop app when it is
                        //   installed, and falls back to the browser otherwise
  pendingStep?: {
    runId: string;
    nodeId: string;
    nodeType: 'ai_agent' | 'ai_judge' | 'consensus' | 'human' | 'round_robin' | 'map';
    prompt: string;     // full prompt — Claude reads this, calls advance_run
    validTransitions: string[];
    label?: string;               // the node's human-readable name
    transitions?: { event: string; target: string; targetLabel?: string }[];
    agentCount?: number;          // consensus: how many subagents to spawn
    model?: string;               // consensus + round_robin: the model to use
    agents?: { model: string; prompt: string }[];  // per-agent diversity
    consensusPhase?: string;      // generate | aggregate | retry
    criterion?: string;           // round_robin: what to judge on
    matchups?: RoundRobinMatchup[];
    roundRobinPhase?: string;
    mapPhase?: string;
  };
  // plus the cost fields: totalCostUsd, meteredCostUsd,
  // subscriptionEquivCostUsd, billingMode, inputTokens, outputTokens
}

Where to next#

  • Using Roscoe over MCP — the same surface as a walkthrough, including how to connect a host to it.
  • CLI commandsroscoe mcp itself, and the commands that share this server's data.
  • REST API — the HTTP surface behind the same runs and workflows.
  • Workflow sources — what source and the global-vs-repo split mean for every tool above.
  • Subscription vs metered — what host decides, and why fan-out nodes hand back instead of calling the API.

View this page as Markdown

Predictable workflows from unpredictable AI