Docs

MCP tools

Full catalog of the tools the Convoy MCP server exposes, with inputs and notable return fields.

Convoy's MCP server speaks JSON-RPC over a single HTTP endpoint at POST /api/mcp (on your Convex deployment's HTTP host). Requests carry a Bearer API key; responses follow the Model Context Protocol. Clients wired up via convoy init mcp --cli <client> already point at this endpoint.

Tools below are grouped by domain. Input fields and types are derived from the server's own tool-definitions module (apps/web/convex/mcp/tools.ts). Return shapes are structured JSON; where the shape is worth calling out, it's noted per tool.

Keep honest. If you change or add a tool in apps/web/convex/mcp/tools.ts, update this page in the same PR. Tool drift here is actively misleading to integrators.

Project-aware URL context

Every tool that takes projectId accepts it as optional when the MCP connection was opened with a ?project=<id> URL parameter. Convoy's convoy init mcp writes that URL parameter into generated configs, so clients connected via the CLI don't need to pass projectId on every call.

When not pre-configured, projectId becomes required for project-scoped tools.

Built-in task type keys (taskTypeKey) come from getBuiltInTaskTypeKeys(): task, bug, feature, epic, investigation, chore, and idea. The default is "task". Additional org-scoped custom types may be present via the web UI; the MCP schema only validates built-in keys.


Projects

list_projects

List all projects the authenticated user has access to.

ArgumentTypeRequiredNotes
No arguments.

Tasks — read

list_task_lists

List project Lists for discovery. Unfiltered persisted Lists are returned in project List rank order. Search queries may use full-text relevance order.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configuredProject to list from.
archivedbooleanNoReturn archived Lists instead of active Lists. Default false.
querystringNoOptional List name/ref search query.
limitinteger 1-50NoDefault 20.
cursorstring | nullNoPagination cursor. Default null.

create_task_list

Create a project List. Requires List management permissions; max 20 active Lists per project. Returns { listId }.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured
namestringYesUnique per project, max 80 chars.
refstringNoUppercase List ref (e.g. SPRINT-1). Derived from the name when omitted. BACKLOG and ARCHIVED are reserved.
colorenumNoColor from the List palette.
defaultViewMode"list" | "board"NoDefault view for this List. Omit to inherit the project default view.
defaultBoardGroupByenumNoBoard grouping when defaultViewMode is board (defaults to status). Ignored otherwise.

update_task_list

Rename, re-ref, recolor, or set the default view of a project List. Requires List management permissions and at least one of name/ref/color/defaultViewMode. System Lists like BACKLOG cannot be updated; archive/unarchive, delete, and default-List changes are not available via MCP. Returns { listId }.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configuredUsed to resolve List refs.
listIdstringYesRaw List ID or bare project-scoped List ref.
namestringNo
refstringNo
colorenum | nullNonull clears the color.
defaultViewMode"list" | "board" | nullNonull clears the override so the List inherits the project default view.
defaultBoardGroupByenumNoBoard grouping when defaultViewMode is board (defaults to status). Ignored otherwise.

list_task_labels

List project labels in board-column order. Returns { labels: [{ _id, name, color }] }. Other tools accept a label as either its raw ID or its exact name (case-insensitive).

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured

list_tasks

List tasks in a project.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configuredProject to list from.
includeArchivedbooleanNoDefault false.
statusesstring[]NoSubset of task statuses. Omit for no status filter; pass [] to return no tasks.
listIdstringNoRaw List ID or bare project-scoped List ref, e.g. SPRINT-1.
includeLinksbooleanNoDefault false. Include each task's externalLinks array in the summaries.

search_tasks

Search tasks in a project with pagination and optional filters.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured
querystringYesSearch query text.
archiveMode"active" | "archived"NoDefault "active".
filters.statusstringNoStatus filter.
filters.assigneeIdstring | nullNoFilter by human owner user ID. Use null for tasks with no owner.
filters.delegatedToAgentIdstring | nullNoFilter by delegated project agent ID. Use null for non-delegated tasks.
limitinteger 1–50NoDefault 20.
cursorstring | nullNoPagination cursor. Default null.
listIdstringNoRaw List ID or bare project-scoped List ref, e.g. SPRINT-1.

Returns paginated results plus a continuation cursor. Pass the returned cursor back via cursor to fetch the next page. The legacy filters.assignee object is no longer accepted (see create_task).

get_task

Get a single task by document ID or ref.

ArgumentTypeRequiredNotes
taskIdstringYesTask document ID or ref.
projectIdstringWhen resolving bare refs without URL-configured project

Returns the task plus its externalLinks (commits, branches, PRs, URLs — see update_task_link), artifact summaries, attached file metadata (with download URLs), the latest up to 50 comments, and direct subtask summaries (each with subtaskCount). Task and subtask payloads include both assignee (the human owner) and delegatedToAgent (the executing agent) projections.

get_tasks

Look up one or more tasks by task IDs or refs (e.g. ABC-5).

ArgumentTypeRequiredNotes
identifiersstring[]YesUp to 25 task IDs or refs.
projectIdstringWhen resolving bare refs without URL-configured project

Returns results in the same order as the input identifiers. Checks active tasks first, then archived. Per-task payload matches get_task.

list_project_agents

List active Project agents available for task delegation.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured

Project agents — write

create_project_agent

Create a project-local Project agent for task delegation.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured
namestringYes
descriptionstringNoShort description.
promptstringNoWorking prompt.
categorystringNoCategory label.
appearanceobjectYes{ color }, using a supported Agent palette color.
automationobjectNo{ enabled, prompt?, cliType?, cliModel?, cliEffort?, mode?, workspace? } assignment-automation config.

Returns { "projectAgentId": "..." }.

update_project_agent

Edit an existing Project agent. Only specified fields are changed.

ArgumentTypeRequiredNotes
projectAgentIdstringYesProject agent ID.
namestringNo
descriptionstring | nullNoPass null to clear.
promptstring | nullNoPass null to clear.
categorystring | nullNoPass null to clear.
appearanceobjectNo{ color }, using a supported Agent palette color.
automationobject | nullNo{ enabled, prompt?, cliType?, cliModel?, cliEffort?, mode?, workspace? } — replaces the whole config; pass null to clear.

Returns null.

Automation config: enabled (boolean, required) makes delegating a task to this agent in the web UI offer to start a prefilled thread; prompt (string) is the plain-text composer prefill; cliType, cliModel, cliEffort, and mode preselect the CLI runtime, model, reasoning effort, and mode; workspace ("local" or "worktree") chooses between the selected checkout and a new worktree. Note that automation only triggers for delegations made in the Convoy web UI — delegating a task to an agent via MCP or the CLI never starts a thread.


Prompts

Prompts (slash commands) exist in two scopes: project prompts belong to a project, organization prompts belong to an organization. All three tools take the same scope arguments:

ArgumentTypeRequiredNotes
scopeenumNoproject (default) | organization.
projectIdstringWhen not URL-configuredProject scope only.
organizationstringNoOrganization scope only: slug or organization ID. Defaults to the organization owning the resolved project.

list_prompts

List active prompts for a project or an organization.

Returns promptId, scope, key, description, category, content, autoExpand, and autoSubmit per prompt.

create_prompt

Create a prompt in a project or an organization.

ArgumentTypeRequiredNotes
keystringYesKey without the leading slash; 2–40 characters of lowercase letters, numbers, hyphens, and colons. Unique per scope owner.
contentstringYesContent inserted into the composer.
descriptionstringNoShort description.
categorystringNoCategory label.
autoExpandbooleanNoDefault false.
autoSubmitbooleanNoDefault false.

Returns { "promptId": "..." }.

update_prompt

Edit a prompt. Only specified fields are changed, and no scope argument is needed — the stored prompt carries its own scope.

ArgumentTypeRequiredNotes
promptIdstringYesPrompt ID.
keystringNo
contentstringNo
descriptionstring | nullNoPass null to clear.
categorystring | nullNoPass null to clear.
autoExpandbooleanNo
autoSubmitbooleanNo

Returns null.

Deleting a prompt is web-UI only.


Skills

A Skill is a SKILL.md plus optional additional files, addressed by relative path. list_skills and create_skill take a scope of project or organization, defaulting to project because an MCP connection is configured per project. get_skill, update_skill, and delete_skill take a skillId, which carries its own scope. An API key narrowed to specific projects cannot reach organization Skills.

Agents read Skills through load_skill and read_skill_file, which see the project's Skills only.

Every write is one save of the Skill, however many files it touches: at most one version bump, and it cannot half-apply. A call that writes the content already stored succeeds without bumping — version is what tells a project its installed copy is behind, so a no-op must not move it. The caps — 50 files, 256 KB per file, 1 MB per Skill — are checked against the whole file set, so a write that breaks one changes nothing and reaches you as a plain tool error rather than an ok: false result.

list_skills and create_skill take the same scope arguments:

ArgumentTypeRequiredNotes
scopestringNoproject (default) or organization.
projectIdstringNoProject scope: the target project, when the connection is not URL-configured. Organization scope: only used to derive the organization default.
organizationstringNoOrganization scope only. Slug or organization ID; defaults to the organization owning the resolved project. Passing it in project scope is an error rather than a silent no-op, so an organization call needs scope: "organization" too.

Every other Skill tool takes a skillId or a Skill name instead.

list_skills

List the project's or the organization's Skills. Returns skillId, scope, name, description, category, and version per Skill — no file content.

get_skill

Get one Skill's whole bundle: the list fields plus skillMd and files ([{ path, content }]). Takes skillId.

create_skill

Create a Skill. Takes the scope arguments above, plus:

ArgumentTypeRequiredNotes
namestringYes2–64 characters of lowercase letters, numbers, and hyphens. Unique within the project or the organization it is created in.
contentstringYesSKILL.md, stored verbatim. Frontmatter in the body is never parsed and never feeds name or description.
descriptionstringNoShown when the Skill is offered to an agent.
categorystringNoCategory label.

Returns { "skillId": "..." }. Add the Skill's other files with update_skill.

update_skill

Edit a Skill: its columns, its whole SKILL.md, and/or its files. Only provided fields change.

ArgumentTypeRequiredNotes
skillIdstringYesSkill ID.
namestringNo
contentstringNoNew SKILL.md, stored verbatim. Use an edits entry to change part of it instead of resending the body.
descriptionstring | nullNoPass null to clear.
categorystring | nullNoPass null to clear.
editsarrayNoFile edits, applied in order. See below.

Returns { "ok": true, "version": 4 } — the Skill's version after the save.

There are no per-file tools: edits is how you touch a Skill's files. Each entry is an object whose op selects the rest of its fields, and every entry takes path, a relative POSIX path — no leading slash, no .. segments.

opAlso takesEffect
writecontentCreate path, or overwrite it if it exists. content may be "" for a placeholder or a truncation.
replaceoldString, newString, replaceAllSwap an exact string inside path. newString: "" deletes the match; replaceAll defaults to false, which requires exactly one match. oldString must match whitespace, newlines, and casing exactly.
movenewPathRename path. Refuses to land on a path already in use.
deleteRemove path.

Edits apply in order to the Skill's file set, and each sees the previous ones' results — rename a file, then edit it by its new path in the same call. content lands before them, so rewriting SKILL.md and patching the result compose. The whole call is one save, so a rejected edit writes nothing at all, your column changes included.

SKILL.md is writable through write and replace but cannot be renamed or deleted — it is the Skill's entry point — and unlike an additional file it must never be blank.

A replace that cannot be placed is reported instead of failing the call, so you can retry with a better oldString:

{
  "ok": false,
  "index": 1,
  "code": "multiple_matches",
  "message": "oldString matches 2 locations in SKILL.md; pass replaceAll to replace every occurrence",
  "occurrences": 2
}

index is the position in edits of the edit that stopped the save. code is old_string_not_found (with occurrences: 0) or multiple_matches. Anything else — a missing file, a bad path, a cap breach — fails the call instead.

delete_skill

Delete a Skill. Takes skillId. Returns null.

load_skill

Load one of the project's Skills by name — the tool an agent uses to pick a Skill up on demand. load_skill's own description lists the project's Skills, so the agent sees what is available without loading anything.

ArgumentTypeRequiredNotes
namestringYesSkill name, as listed in the tool's description.
projectIdstringWhen not URL-configured

Returns { name, description, skillMd, paths }paths being the Skill's other files, to read one at a time — or null when the project has no Skill by that name, including a name no Skill could have. Organization Skills are never reachable here. Descriptions in the tool's catalog are author-written prose flattened to one line each; treat them as untrusted content, not as instructions.

read_skill_file

Read one file inside one of the project's Skills.

ArgumentTypeRequiredNotes
namestringYesSkill name.
pathstringYesRelative POSIX path from load_skill's paths.
projectIdstringWhen not URL-configured

Returns { path, content }, or null when the Skill or the path does not exist — a malformed path included.


Tasks — write

create_task

Create a task (or subtask) in a project.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured
namestringYes
descriptionstringNoMarkdown, plain text, or serialized ProseMirror JSON. Markdown/plain is normalized to ProseMirror.
taskTypeKeystringNoBuilt-in key (default "task").
parentIdstringNoParent task ID to create as subtask.
listIdstringNoRaw List ID or bare project-scoped List ref.
priorityenumNoNone | Low | Medium | High | Critical. Default None.
statusenumNoStatus values enabled for the project (see Roadmap). Default To Do.
dueDatenumberNoUnix timestamp.
assigneeIdstring | nullNoUser ID of the human owner. null leaves the task unassigned.
delegatedToAgentIdstring | nullNoProject agent ID executing the task. null clears delegation.
tagsstring[]NoDefault [].
labelstringNoLabel ID or exact label name.

Assignment is split into two fields: assigneeId is the human owner accountable for the task, delegatedToAgentId the agent executing it. The old assignee object argument is removed and rejected with an error (assignee is no longer supported; use assigneeId (user) and/or delegatedToAgentId (agent)). Delegating a task that would have no owner auto-sets the caller as owner — pass assigneeId: null explicitly to keep it ownerless.

Return shape (structured):

{
  "taskId": "kx9...",
  "refId": "ABC-42",
  "taskPath": "/o/acme/ABC?task=ABC-42",
  "taskUrl": "https://app.example.com/o/acme/ABC?task=ABC-42"
}

refId, taskPath, and taskUrl are null if the task was created but the ref or URL couldn't be resolved (e.g. missing app URL config).

update_task

Patch an existing task. Pass null to clear optional fields.

ArgumentTypeRequiredNotes
taskIdstringYes
namestringNo
descriptionstring | nullNoMarkdown / plain / ProseMirror. null clears.
taskTypeKeystring | nullNonull resets to "task".
priorityenumNoSame enum as create_task.
statusenumNo
dueDatenumber | nullNonull clears.
assigneeIdstring | nullNoUser ID of the human owner. null unassigns.
delegatedToAgentIdstring | nullNoProject agent ID executing the task. null clears delegation.
tagsstring[]NoReplaces the tag list.
labelstring | nullNoLabel ID or exact label name. null clears.

Assignment follows the same split model as create_task: the old assignee object argument is rejected, clearing delegation keeps the owner, changing the owner keeps the delegation, and delegating a task with no owner auto-sets the caller as owner unless assigneeId is passed explicitly (assigneeId: null keeps it ownerless).

create_task_label

Create a project label. Requires project admin permissions; max 20 labels per project. Returns { labelId }.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured
namestringYesUnique per project, max 50 chars.
colorenumNoColor from the extended palette. Default blue.

update_task_label

Rename or recolor a label. Tasks keep the label (they reference it by ID). Requires project admin permissions.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configuredUsed to resolve label names.
labelIdstringYesLabel ID or exact label name.
namestringNo
colorenumNoColor from the extended palette.

delete_task_label

Delete a label and resolve every task that references it. Requires project admin permissions. Returns { affectedTaskIds }.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configuredUsed to resolve label names.
labelIdstringYesLabel ID or exact label name.
resolutionenumNoclear (default) removes the label from tasks; reassign moves them to reassignTo; archiveTasks clears the label and archives active tasks.
reassignTostringFor reassignTarget label ID or exact name.

Upsert or delete a single external link on a task (commit, branch, pull request, or generic URL). Returns { taskId, externalLinks } with the full updated list.

ArgumentTypeRequiredNotes
taskIdstringYesTask document ID or ref.
projectIdstringWhen resolving bare refs without URL-configured project
openumYes"upsert" or "delete" (idempotent).
idstringYesStable caller-generated link ID (1–160 chars of a-zA-Z0-9._:-). Use deterministic IDs like github-pr-owner-repo-123 so repeated upserts update instead of duplicating.
linkobjectFor upsertKind-discriminated payload, see below.

Link payload kinds — required, then optional fields:

kindRequiredOptional
commitsha (7–64 hex)title, repo, branch, url
branchnamerepo, url
pull_requesturltitle, identifier, repo, branch
urlurltitle

Upsert replaces the whole link in place (original createdAt is preserved). URLs must be http(s). Tasks hold at most 20 links.

move_task

Move a task under a new parent, or back to top-level.

ArgumentTypeRequiredNotes
taskIdstringYesTask document ID, bare ref, or scoped ref like org-slug/ABC-5.
parentIdstring | nullYesNew parent ref or ID; null moves the task to top-level.
projectIdstringWhen using bare refs without URL-configured project

LexoRank generation stays server-side — callers don't pass a rank.

move_tasks_to_list

Move one or more existing tasks into an active project List.

ArgumentTypeRequiredNotes
taskIdsstring[]YesUp to 25 task IDs or refs. Parent/child overlap is rejected.
listIdstringYesRaw List ID or bare project-scoped List ref.
projectIdstringWhen resolving bare refs without URL-configured project

delete_task

Soft-delete a task. Deleted tasks are permanently removed after 30 days.

ArgumentTypeRequiredNotes
taskIdstringYes
cascadeMode"delete_all" | "keep_children"NoDefault delete_all. keep_children reparents subtasks up one level.

Task types

list_task_types

List the built-in and org-scoped task types available to this API key.

ArgumentTypeRequiredNotes
No arguments.

Comments

create_task_comment

Create a markdown comment on a task.

ArgumentTypeRequiredNotes
taskIdstringYes
contentstringYesMarkdown. Must be non-empty after trimming.

Artifacts

Artifacts are durable document deliverables (markdown or html) attached to a task (plans, reviews, handoff docs). Distinct from comments and attachments — see Using Convoy for the product-level distinction.

list_task_artifacts

List artifacts for a task.

ArgumentTypeRequired
taskIdstringYes

get_task_artifact

Get a single artifact by ID, optionally with its comment threads.

ArgumentTypeRequiredNotes
artifactIdstringYes
includeCommentsbooleanNoAdds a comments array of threads (same shape as list_artifact_comments). Default false.
commentsStatusstringNoWith includeComments, only threads whose root is open or resolved. Default: all.

create_task_artifact

Create a document artifact on a task.

ArgumentTypeRequiredNotes
taskIdstringYes
namestringYesArtifact title.
contentstringNoArtifact body. Default empty string.
contentTypestringNomarkdown (default) or html. Immutable after creation. html artifacts render read-only in the UI.
runIdstringNoOptional provenance identifier.

update_task_artifact

Patch an existing artifact. Only provided fields are changed.

ArgumentTypeRequiredNotes
artifactIdstringYes
namestringNo
contentstringNo
runIdstring | nullNonull clears an existing runId.

replace_in_task_artifact

Replace an exact string in artifact content.

ArgumentTypeRequiredNotes
artifactIdstringYes
oldStringstringYesExact existing string. Must be non-empty.
newStringstringYesReplacement string. May be empty.
replaceAllbooleanNoDefaults to false; when false, exactly one match is required.

Zero-match and multiple-match failures return an MCP tool error (isError: true) inside a successful JSON-RPC response.

move_task_artifact

Move an artifact to another task in the same project. The artifact leaves the source task, taking its comments with it.

ArgumentTypeRequiredNotes
artifactIdstringYes
taskIdstringYesTarget task ID or ref (e.g. ABC-5). Must be in the same project.
projectIdstringNoRequired to resolve a ref on a connection with no ?project= default.

delete_task_artifact

Delete an artifact.

ArgumentTypeRequired
artifactIdstringYes

Artifact comments

Comments anchored to text snippets in markdown artifacts. Each thread is a root comment (with an anchor and an open/resolved status) plus flat replies.

list_artifact_comments

List an artifact's comment threads.

ArgumentTypeRequiredNotes
artifactIdstringYes
statusstringNoOnly threads whose root is open or resolved. Default: all.

Returns { threads: [...] }; each thread carries commentId, status, anchorText (the quoted snippet), authorName, content, createdAt, isEdited, and replies.

create_artifact_comment

Comment on a text snippet in a markdown artifact.

ArgumentTypeRequiredNotes
artifactIdstringYes
contentstringYesComment body (markdown).
anchorTextstringYesExact snippet from the artifact content. Whitespace, newlines, and casing must match.
occurrencenumberNoWhich match of anchorText to use when it appears multiple times (1-based, default 1).

The server derives the full anchor (context + offsets) from anchorText; fails if the text is not found in the artifact content.

reply_to_artifact_comment

Reply to a root artifact comment. Replies are flat — replying to a reply fails.

ArgumentTypeRequired
commentIdstringYes
contentstringYes

set_artifact_comment_status

Resolve or un-resolve root artifact comments in one atomic batch.

ArgumentTypeRequiredNotes
commentIdsstring[]YesRoot comment IDs (1–100).
statusstringYesopen or resolved.

CLI profiles

list_cli_profiles

List your CLI profiles (connected machines) in a project, with online state, the CLI runtimes each one advertises, and the checkouts it exposes. This is how an agent picks a target before spawning a CLI thread.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configured

Per profile, returns profileId, name, hostname, isOnline, lastConnectedAt, appVersion (the @useconvoy/cli version the online session reported), availableClis ({ type, version, provider?, cliModels? }type is a cliType, and cliModels is { model, efforts? }[]: the cliModel values that runtime accepts on that machine, each with its valid cliEffort values; omit either to use the runtime's own defaults), and paths ({ pathId, path, label, gitRemoteUrl?, kind?, parentPathId?, branch? }, where kind: "worktree" marks a worktree rather than a repo checkout).

API keys are personal, so this returns only profiles you own. An offline profile is still a usable target — work queued against it starts once that CLI connects.


Threads

get_thread

Get one CLI thread: its title, status, queue state, and the target it runs on. An agent running inside a thread is told its own ID at session start, so this is how it discovers its own machine and checkout before spawning another thread there.

ArgumentTypeRequiredNotes
threadIdstringYesA thread ID, or a thread group / workflow run container ID.

No projectId — a thread ID is globally unique and the project comes from the thread.

For an ordinary thread, returns threadId, title, status, isDone, createdAt, lastActivityAt, associatedTaskId, parentThreadId (the thread group it belongs to, if any), spawnedByThreadId (the thread whose agent created it), the target — profileId, pathId, path, cliType, profileName — and its queue state: processingState, hasPendingUserMessages, hasPendingInteraction, queueHeldAt. targetOnline and lastHeartbeatAt describe the target machine; an offline target just means work stays queued until that CLI connects.

For a thread group or workflow run container, returns kind, childCount, aggregateStatus (idle, running, needs_attention, or failed), and originThreadId — and no target, because a container has nothing to execute and cannot receive messages. It also returns no status: aggregateStatus is a container's only status, and reflects its children. Pass the container's ID to list_threads for the members.

A thread that does not exist, one belonging to someone else, and an ID that is not a thread ID at all all come back as a "Thread not found" error. Threads shared with you by a project teammate are readable, including the individual threads inside a shared group.

list_threads

List the CLI threads in a project, most recent activity first — or the members of one thread group.

ArgumentTypeRequiredNotes
projectIdstringWhen not URL-configuredProject ID. Not needed when threadId is set.
threadIdstringNoA thread group / workflow run container ID: list that group's members instead of the project's threads.
includeDonebooleanNoInclude threads marked done. Default false.
limitintegerNoPage size, 1–50. Default 20. Ignored when threadId is set.
cursorstring | nullNoPagination cursor. Default null. Not used when threadId is set.

Every row carries the same fields as get_thread, so you can act on a listed thread — send it a message, spawn a sibling on its machine — without a second call.

A thread group or workflow run appears as a single row (kind, childCount, aggregateStatus) rather than as its members, so a page of 20 rows is 20 separate pieces of work. A row with kind set is one you can drill into: pass its threadId back in to list the members, which come back as ordinary threads with full targets.

A group is capped at 100 members and the drill-down returns all of them in one page — limit and cursor apply to the project list only. So a group can come back larger than the limit you asked for. Drilling into a group needs no projectId either: the container ID is enough to find it.

Threads you have marked done are left out unless you set includeDone.

Only your own threads appear in the project list, even in a project you share with teammates. The exception is drilling into a group a teammate shared with you: its members are readable, the same as opening the group in the web app.

An ID you cannot read — a deleted group, someone else's, a typo'd one, or an ordinary thread rather than a group — returns an empty list rather than an error. Use get_thread when you need to know whether a specific ID is valid.

list_thread_messages

Read the most recent messages in a CLI thread, oldest first — what the agent said, and what was said to it.

ArgumentTypeRequiredNotes
threadIdstringYesA thread ID. Not a thread group / workflow run container ID.
limitintegerNoHow many of the most recent messages to return, 1–20. Default 10.

No projectId — as with get_thread, the thread ID is enough.

Per message, returns messageId, role (user or assistant), status, createdAt, text, truncated (only when something was cut), sentBy (present when another thread's agent queued the message rather than a person), and errorMessage (the CLI's reason, on the message the failing run was working on). A status of streaming means the text is whatever had arrived when you read it.

text is the agent's text output only. Tool calls, tool output, reasoning, and attachments are not included, so a message that only ran tools comes back with empty text — an empty entry, not an error, and not a sign the thread is quiet. Each message is capped at about 4000 characters. truncated: true marks a message you did not receive in full; it fires on rows that went unread, which on a tool-heavy run includes rows that held no prose.

There is no cursorlimit is the whole window, so the older turns of a long thread are not reachable through this tool.

A thread group or workflow run returns an error rather than an empty list: it has no transcript and never will. Pass its ID to list_threads for the members and read one of those. A thread you cannot read — missing, someone else's, or a typo — returns an empty list, the same as list_threads. Threads shared with you by a teammate are readable, including the threads inside a shared group.

create_thread

Start a new CLI thread with an initial message. An agent running inside a thread is told its own ID at session start, so passing that as sourceThreadId is how it hands work to a second agent on the same machine and checkout.

ArgumentTypeRequiredNotes
contentstringYesThe initial user message — the new thread's prompt.
sourceThreadIdstringNoSpawn beside this thread, inheriting its target. Accepts a thread group container ID.
profileIdstringWithout a sourceTarget machine, from list_cli_profiles.
pathIdstringWithout a sourceCheckout on that profile. Also required when profileId names a different machine than the source's.
cliTypestringWithout a sourceCLI runtime, from that profile's availableClis.
titlestringNoDefaults to a title derived from content.
taskIdstringNoTask ID or ref (e.g. ABC-5) to attach the thread to.
projectIdstringNoOnly to resolve a bare taskId ref when the connection has no default project. Must match the target profile's project.
cliModelstringNoModel for the runtime to use.
cliEffortstringNoReasoning-effort level, for runtimes that support one.
cliModestringNoCLI mode to start in, e.g. plan.
runtimeCommandbooleanNoOpen the thread by running content as the target agent's own slash command instead of sending it as a prompt.
projectAgentIdstringNoProject agent to run the thread as, from list_project_agents.
worktreeobjectNoRun in a fresh git worktree: { parentPathId, branch, baseRef?, startFromOrigin? }.

With sourceThreadId, the target — profileId, pathId, cliType — is inherited from that thread. Without a source, all three are required.

cliType can be sent on its own to change just the runtime. pathId, though, only identifies a checkout on its own profile — path IDs are not shared between machines — so spawning onto a different profileId means sending its pathId too, or the call is rejected.

Spawning groups the two threads together. The source thread's row in the sidebar becomes a thread group with tabs, and a pin or a share on it moves to the group. That is the same thing that happens when a person adds a thread from the group's "+" button — only the initiator differs. sourceThreadId also accepts a thread group container ID, meaning "add a thread to this group"; a member is chosen for you. A workflow run cannot be joined.

A group cannot span projects, so the target profile has to be in the source thread's project.

A group holds at most 100 threads, and that count includes deleted ones — so a group can be full with far fewer than 100 threads visible, and retrying will not help.

worktree starts the thread in its own branch. Without it the thread runs in the checkout itself, sharing the working tree with anything else running there. With it, the CLI creates a worktree when it claims the thread and runs there instead. The parent is the checkout the thread would otherwise have used, so parentPathId must equal the effective pathId — the one you sent, or the one inherited from sourceThreadId. A worktree cannot be a parent, so spawning beside a thread that is already in a worktree means sending the parent repo's pathId yourself; list_cli_profiles reports each path's kind and parentPathId. branch and baseRef are validated as git refnames and never sanitized for you. baseRef defaults to the repo's current HEAD; startFromOrigin starts from the remote's copy of it instead.

projectAgentId runs the thread as a Project agent, from list_project_agents. The agent has to belong to the target profile's project. Its prompt is snapshotted onto the thread at creation, so later edits to the agent do not change a running thread, and the binding cannot be changed afterwards. The agent's assignment-automation defaults — runtime, model, worktree preference — are not applied here; they belong to the web UI's composer, so worktree is always an explicit request.

Neither field is inherited from sourceThreadId, unlike the target tuple. A spawned thread runs unbound, in the source's checkout, unless you say otherwise. Neither is echoed back in the response either: the returned summary shows the parent path and no worktree or agent field, so treat the absence of an error as the receipt.

Returns the new thread exactly as get_thread would. Check targetOnline: the thread starts pending, and if the target machine is offline it stays queued until that CLI connects. The spawned thread does not run inside your process and you will not see its reply — read it later with list_thread_messages, or have that agent report back.

Attachments are not supported here. Put the content inline in content, or a path the target machine can read — which is your own machine only when you spawned from a source thread on it.

Opening on the target's own slash command. Set runtimeCommand: true to start the thread by running a command the target agent already has (/security-review), rather than sending content as a prompt. The same rule as send_thread_message applies: content is the whole command and must start with /. Convoy prompts from list_prompts are not these — pass their content as ordinary content.

Only claude-code and codex targets accept it. cursor and opencode inject the thread's identity and its bound project agent's prompt into the first message of a fresh session, so a raw command there would either displace that bootstrap or skip it for the thread's whole life — those cliTypes are rejected outright. Send the command with send_thread_message once the thread has run instead. On codex the only executable command is /compact, which does nothing on a thread that has no context yet, so in practice this is a claude-code feature.

Every spawned thread is a real agent run that costs money. The tool description asks an agent to spawn only what it was told to, to report back rather than spawn again if it is itself a spawned thread, and not to spawn just because its prompt discusses delegation — a spawned agent given a prompt that described spawning has been observed doing exactly that. That guidance is cooperative; enforced spawn limits are tracked separately.

send_thread_message

Queue a message in a thread that already exists. This is how a spawned agent reports back to the thread that spawned it, and how one agent nudges another.

ArgumentTypeRequiredNotes
threadIdstringYesThread to post into. Not a thread group or workflow run container ID.
contentstringYesThe message text, delivered as a user message.
sourceThreadIdstringNoYour own thread ID, so the message shows as sent by an agent rather than a person. Must be in the same project as threadId.
runtimeCommandbooleanNoRun content as the target agent's own slash command instead of sending it as text.

It does not resume a paused agent. The message is queued and the target CLI starts a new run — one that resumes the runtime session, so the agent keeps its prior context and reads your message as a fresh user turn.

That has a consequence worth planning around: "spawn a thread and wait for its answer" does not work. Your turn ends after spawning; the reply arrives later as a message that wakes your thread up again. You will not see it in the same turn, and if you want to check for it yourself, poll list_thread_messages.

Check hasPendingUserMessages, not status, to confirm a send landed. An idle target becomes pending; a target that is mid-run keeps its current status and picks the message up on its next turn. Both queued the message. An offline target queues it until that CLI connects. None of these is an error.

Only your own threads count, as source and as target, and both have to be in the same project. Thread groups and workflow runs can neither send nor receive — pass a group ID to list_threads and use one of its members.

To make a spawned agent report back, say so in the prompt you give it and include your own thread ID. Every runtime is told its thread ID at the start of a session — usually in the CONVOY_THREAD_ID environment variable, though OpenCode receives it in the prompt instead. Nothing enforces the report-back; the agent has to cooperate.

Running the target's own slash commands. Set runtimeCommand: true and the CLI hands content to the agent as a command instead of as text, so /compact compacts that thread's context. content must be the whole message and start with /; anything else is rejected. This is the same mechanism the web composer's / menu uses, so it works where that does — /compact on claude-code, codex and opencode threads.

There is no way to list a thread's commands over MCP, and an unrecognized command behaves differently per runtime: claude-code and cursor also run their own custom commands and skills, opencode fails the run on a name it does not know, and codex executes only /compact — any other command reaches it as ordinary text, which it may answer as if it had run.


Authentication and errors

  • Auth. Every request must carry Authorization: Bearer <api-key>. API keys are user-scoped and inherit that user's Convoy permissions (see API keys).
  • Error envelope. Errors follow the JSON-RPC 2.0 shape. Common codes: -32700 (parse error), -32001 (unauthorized), -32603 (internal error).
  • Permission errors. These surface as structured errors with human-readable messages; the MCP layer does not invent new permissions — authorization still runs through Convoy's organization / project permission helpers.

See also

On this page