- Documentation
- Integrations
- AI agents (MCP)
- MCP tools and limits
MCP tools and limits
Every tool the TaskJuice MCP server exposes, its arguments, its errors, and the per-plan call budgets.
The server exposes at most 17 tools per connection, all of them listed below. A workflow is never its own tool: an assistant finds the workflows you have exposed with find, reads one with describe, and runs it with run_workflow, passing the workflow's slug.
Each tool needs one permission — the Permission column below. A connection is only offered the tools whose permission it holds, on top of your plan, your role, and the connection URL (see Choose what a connection can do). Every connection holds Read.
Always-available tools
Available to sign-in connections and API keys, on every plan.
| Tool | Permission | Arguments | Returns |
|---|---|---|---|
whoami | Read | none | Who the connection is and what it can do here — see The whoami result. |
find | Read | type, query?, cursor?, limit? (≤ 50); for runs also status?, workflowSlug?, workflowId?; for nodes also kind?; for drafts, nodes, apps, tables and app connections also workspace? | Searches one type of item a page at a time. type is workflow (ranked by how well the name and descriptions match query), draft, run (newest first, filtered by status, workflowSlug or, on a connection that can author, workflowId; runs take no query), skill, node (the catalog's app actions and triggers, built-in steps and AI sub-nodes, matched by meaning as well as words, so "send a text" finds SMS actions; narrow with kind: app-action, app-trigger, system or capability), app (catalog apps, each with needsConnection), form, table, template or connection (see Read forms, tables, templates and app connection health). Omit query to list everything. Each page returns items, each with the id to pass to describe (a workflow item also carries its workflowId, which create_workflow_draft takes to open a draft of it), plus total (matches across all pages; null for runs), nextCursor (pass it back as cursor for the next page; null on the last one) and truncated (true when there are more items than one search reads, so the rest weren't searched). Drafts, nodes, apps, forms, tables, templates and app connections need a connection that can author (see Authoring tools); skills need a plan with managed skills. On the whole-account URL, nodes, apps, tables and app connections are searched in the workspace workspace names (its slug), unless the account has only one. Each draft names its workspace's slug in workspace, and workspace narrows the draft list to one workspace. |
describe | Read | id, type?; for nodes also host?; for drafts, deployments, nodes, apps, tables and app connections also workspace? | One item in full, by the id find returned (workflow:send_invoice, run:<run id>, skill:<slug>, node:<canonical>, app:<slug>, form:<id>, table:<id>, template:<id>, connection:<id>) or by a workflow's id (deployment:<workflow id>): a workflow's input schema with its source, one example input and its workflowId, a draft's graph and validation, a run's status and output, a skill's current instructions, a catalog node's config schema, every step an app offers and whether it still needs a connection, a workflow's deployment status (see Check whether a workflow is live), a form's fields, a table's columns, a template's steps, or an app connection's health. Find catalog nodes and apps with find. Drafts, nodes, apps, deployments, forms, tables, templates and app connections need a connection that can author; skills need a plan with managed skills. An id from another tool, such as a workflow slug or a run id, works when you also pass type. On the whole-account URL, a node, an app or an app connection is read in the workspace workspace names, unless the account has only one; a draft, a deployment or a table decides its own workspace, and a workspace that does not match it answers not-found. |
run_workflow | Run | workflowSlug, input?, idempotencyKey? | Waits up to 25 seconds. A finished run returns its output; a longer run returns {status: "running", runId} — follow up with describe({ id: "run:<runId>" }). An input that doesn't match the workflow's schema returns invalid-input naming the field to fix (extra fields always pass through; nothing is dispatched and the idempotency key is not consumed). |
control_run | Run | runId, action | Stops or resumes one run, by the runId run_workflow or find({ type: "run" }) returned. action is { kind: "cancel", reason? } (ends the run for good: steps that have not started never run; the reason, up to 500 characters, shows on the run page) or { kind: "resume" } (continues a run a person paused). A run already in the requested state answers noOp: true and nothing changes. A run that already finished, and resuming a run waiting for an approval, are refused with run-control-refused; an approver decides an approval in TaskJuice, and cancelling such a run still works. Cancel does not stop a step that is already running (it finishes), AI agent work already under way, or a phone call in progress. Not available to a client's own sign-in. |
Details worth knowing:
inputis the trigger payload. Whatever the agent passes arrives at the workflow exactly as a webhook body would.describe({ id: "workflow:<slug>" })shows the expected shape and one example input.idempotencyKeyprevents double runs. The same key from the same connection within 24 hours returns the original run instead of dispatching again. Use it whenever an agent might retry.- Outputs are capped at 256 KB. Larger outputs come back truncated with
outputTruncated: true; the full result stays in the run detail view. - Two workflows with the same name get distinct slugs suffixed with the workflow id's first 8 characters, so slugs never collide. Exposing a second workflow with the same name changes the first one's slug too.
The whoami result
| Field | What it holds |
|---|---|
account | The account: id, name, slug. |
workspace | The workspace the URL names, the same shape; null on a whole-account URL. |
connectionUrl | The URL this connection uses. |
credential | kind: "oauth" with the signed-in email, or kind: "api-key" with the key's label and expiresAt. |
permissions.granted | The permissions this connection holds, as read, run, build, admin. |
permissions.source | granted (chosen when it connected), legacy-default (made before permissions existed), or unreadable-claim (the grant couldn't be read, so only Read applies). |
permissions.statuses | One entry per permission: granted, usable here, and blockedBy when it isn't — not-granted, api-key (keys hold Read and Run only), role (authoring needs an account role), or plan. |
dailyBudget | The account's tool calls today (tenant), the key's own cap if it has one (credential), and the tighter of the two as remaining. null for a sign-in user with only workspace access (no account role). control_run calls are not counted. |
workspaces | On a whole-account URL: up to 50 workspaces this credential can connect to, each with its mcpUrl, and truncated if there are more. null on a workspace URL, and for a key with a workflow allowlist. |
Skills tools
Available when your plan includes managed skills. List the skills licensed to a connection with find({ type: "skill" }).
| Tool | Permission | Arguments | Returns |
|---|---|---|---|
get_skill | Read | skillSlug | One skill's current instructions, with the slug of each workflow it runs through run_workflow on this connection. |
Authoring tools
Available only to sign-in connections, on a workspace URL or the whole-account URL, on plans that include authoring. A call about an existing workflow, draft or table works in that item's own workspace; making something new names its workspace with workspace (see One connection for every client).
| Tool | Permission | Arguments | Returns |
|---|---|---|---|
create_workflow_draft | Build | name or workflowId, description?, type?, workspace? | A new empty draft: ids, the draft's graph hash, an editor link, and the workspace it was created in (draft.workspace). The workflow goes in the workspace workspace names (its slug), or the only one; on the whole-account URL of an account with more than one, leaving it out creates nothing and answers invalid-arguments listing the workspaces. With type: "form" (forms belong to the whole account, so no workspace): a draft form with one starter field, returned as describe shows a form plus editorUrl (the form builder). Nothing is public until it is published. See Build a form. With workflowId instead of name: opens a draft of a workflow that is already published — see Change a published workflow. |
propose_changes | Build | workflowId, changeSet, baseGraphHash?; for a form type: "form", formId, changeSet, baseFormHash? | Applies a change-set all-or-nothing. Rejections list every violation; success persists and returns the new graph hash. For a form, applies a form change set to a draft form and returns the saved form with its new formHash. |
validate_draft | Read | workflowId | Judges the draft against the same gate the in-app drafter uses. Red names the blocker. Green lists what a person must still finish before publishing — app connections on the workflow's Setup page, the rest in the editor; none of it blocks the draft: unconnected apps (needsConnection), unset selections such as a phone number or form (needsResources), a Voice Agent whose Model sub-node needs an LLM key (needsModelKeys), and credentials finished on the node, such as a provider's public key (needsCredentialSetup). Green may also list warnings: things that do not stop the draft or publish but that a person should see, such as a Branch whose no-match path leads nowhere. Each names its step (stepName). |
preview_mapping | Read | expression, sampleOutput, bindings? | Evaluates a JSONata mapping against sample data and returns the output plus warnings. |
test_step | Build | workflowId, nodeId, input?, workspace? | Tests one step of the newest draft, the same as the editor's Test button, and returns its output, its error and Data In. The step runs on its sample data: input if you pass it, else the step before it's pinned output, else its own pinned input; $trigger and $steps come from the draft's pinned samples, and inputsUsed says which were used. The step runs for real against the workspace's connections (an email step sends), and nothing is saved to the draft. Triggers, notes and sub-nodes can't be tested this way. |
edit_table | Build | create or tableId, changes, previewHash?, workspace? | Creates a table with its columns, or changes an existing table's columns: add-column, rename-column, retype-column, remove-column. Live as soon as it saves. Returns the table as describe does, with every column id. Removing a column takes the confirm step — see Build and fill tables. A new workspace table goes in the workspace workspace names, as for create_workflow_draft; an account table and a change by tableId take no workspace. |
table_rows | Build | tableId, action, previewHash? | One table's rows, one action per call: query, insert, update or delete, up to 50 rows. Deleting takes the confirm step — see Build and fill tables. |
The authoring tools read through describe and find:
- A draft:
describe({ id: "draft:<workflowId>" })returns its graph, its hash (the base forpropose_changes), validation state (with anywarnings, asvalidate_draftreturns them), what a person still has to finish — app connections on the workflow's Setup page, the rest in the editor (seevalidate_draft), and the editor link. - A node:
describe({ id: "node:<canonical>" })returns one node's configuration schema. An unknown canonical returnsfound: falsewith a hint — not an error. For the capability sub-nodes (capability-model,capability-tool,capability-memory) it returns their contract: which agent nodes accept them, the exact edge to add, and their config schema.capability-modelalso listseligibleModels; passhost: "taskjuice.system.voice_agent"to get the realtime models a Voice Agent can run. - The catalog:
describe({ id: "app:<slug>" })lists every step one app offers and whether it still needs a connection;find({ type: "node", query })searches every app's steps, the built-in system nodes and the capability sub-nodes.
Agents propose, the platform validates — an invalid change never persists. Connecting accounts, attaching keys, and anything else that touches a credential always happens in the app, from the links the tools return.
Change a published workflow
Once a workflow is published it usually has no draft, so propose_changes and describe({ id: "draft:<workflowId>" }) answer not-found for it. Open one first:
create_workflow_draft({ workflowId })opens a draft of the workflow. It is a copy of the live version, or, when the workflow is switched off, of its newest switched-off version. Leavename,descriptionandworkspaceout: the workflow already has them. Each itemfind({ type: "workflow" })returns carries the workflow'sworkflowId, and so doesdescribe({ id: "workflow:<slug>" }). It is also the onecreate_workflow_draftreturned when the workflow was first drafted, and once the workflow has run, its runs (find({ type: "run" })) carry it too.- The result has the same shape as a new draft's:
createdistrue,sourceVersionis the version the draft was copied from, andversionis the draft's own number. If the workflow already has a draft, you get that draft back withcreated: falseandsourceVersion: null, and nothing new is made. - Read it with
describe({ id: "draft:<workflowId>" }), change it withpropose_changes, thenpreview_publishandpublish. The live version keeps running until the publish replaces it.
A workflow with no draft and no version that ever went live has nothing to copy, so the call answers not-found and points to the editor. While a publish of the workflow is still running (compiling, or waiting on its evaluations), the call opens nothing and answers deploy-refused with refusalCode: "version-in-progress"; try again once describe({ id: "deployment:<workflowId>" }) shows it has finished. Opening a draft needs the Build permission and, like creating a workflow, a plan that allows it.
Publish and state tools
Available to the same connections as the authoring tools.
| Tool | Permission | Arguments | Returns |
|---|---|---|---|
preview_publish | Read | workflowId, or type: "form" and formId | What publishing would do, without doing it: every blockers entry, the sideEffects going live has (each marked reversible and externallyVisible), the setupRequired steps a person must take, canPublish, and a previewHash. Non-blocking warnings (for example a Branch or Switch whose no-match path leads nowhere) are listed too; they never change canPublish or the previewHash. For a form: its blockers, the public link (publicUrl), the fields added, removed or changed since it was last published (changes, with plain warnings), canPublish and a previewHash. |
publish | Admin | workflowId (or type: "form" and formId), previewHash | Publishes and turns the workflow on — but only if nothing changed since the preview with that hash. Returns outcome (active, already_active, or pending_eval while evaluations run), the after-publish steps, and a next sentence saying what to do. For a form: its public page starts taking submissions; returns publicUrl and a next sentence on starting a workflow from it. |
set_workflow_state | Admin | workflowId, change, dryRun?, previewHash? | Makes one change: { "kind": "activate" }, { "kind": "deactivate" }, { "kind": "expose", "description"?: "…" } (make it findable with find and runnable with run_workflow), or { "kind": "unexpose" }. dryRun: true returns the effects and changes nothing. |
delete_workflow | Build | workflowId, previewHash? | Deletes a workflow that is turned off, for good, with its versions; its run history will no longer be viewable. The first call deletes nothing and returns preview-required — see Deleting takes the confirm step below. Once deleted, returns the workflow's name, versionsDeleted and a next sentence. |
How the loop works:
- Preview, then publish with the hash.
publishrefuses withpreview-staleif the workflow, your plan's headroom, or its evaluations changed after the preview — callpreview_publishagain and show the person what changed. Retrying apublishthat already succeeded returnsalready_active, so a dropped connection never publishes twice. - Setup steps are data, never secrets. Each
setupRequiredstep has a stableid, plain-languageinstructions, and ahumanLinkthat opens where a person finishes it: the workflow's Setup page for connecting an app, otherwise the exact node (or page) — attaching an LLM key to a Voice Agent's Model sub-node, picking a phone number or form, pasting a provider's key, or entering a webhook signing secret. Steps markedbefore_publishmust be done first;publishrefuses withrefusalCode: "setup-required"until they are. Steps markedafter_publish— such as registering the webhook URL with the sending system (the step carries the URL) — are for the person to do once it is live. - Apps are connected on the Setup page. Each workflow has a Setup page that lists every app its draft still needs, and a person reaches it from a connection step's
humanLinkafter signing in. Connecting an app there, or asking the client to connect it, fills every step of that app in the draft at once, so the person never opens the editor once per step. A connection the Setup page can't add, such as one for a tool attached to an AI Agent, keeps its link to the node in the editor. - In-chat setup card. In assistants that support MCP Apps,
validate_draftandpreview_publishalso show a small setup card in the chat, drawn from thesetupStatusfield of their result. It lists each app still to connect, with a Set up button that opens the workflow's Setup page for that app, the number of other items the assistant will walk the person through, and the apps waiting on your client, which never hold publishing back. While it is on screen and setup is not finished, the card checks again by callingvalidate_draft, first every 5 seconds and then less often, for at most 40 checks (about 17 minutes); these checks count against the usual limits. When the last item is done, it posts one message into the chat so the assistant can carry on and publish. Assistants without MCP Apps ignore the card, andsetupStatusis in the result for any assistant to read. - Irreversible effects need agreement. Registering a webhook with a provider, or emailing queued connection requests to your clients, cannot be undone by turning the workflow off.
set_workflow_stateasks for apreviewHashbefore an activation with such an effect, or an expose or unexpose that changes another workflow's slug: without one it returnspreview-requiredwithdetails.mustConfirm, one sentence per effect to show the person. Turning a workflow off never needs one. - Turning on runs the version that was last live.
set_workflow_statewithactivateturns a workflow back on with the version that was running when it was turned off, not the newest saved one. If newer saved changes exist, the result'sunpublishedChangesnames both versions and carries amessageto show the person, on both the dry run and the apply. Those changes stay unpublished until you callpreview_publish, thenpublish. - Deleting takes the confirm step.
delete_workflowwithoutpreviewHashdeletes nothing: it returnspreview-requiredwithdetails.mustConfirm— the workflow's name, how many versions go with it, that its run history will no longer be viewable, and, if it was ever published, that its triggers (schedules and webhooks) are removed for good — anddetails.freshHash. Show the person those sentences; if they agree, calldelete_workflowagain withpreviewHashset todetails.freshHash. If the workflow or any of its versions changed in between, the second call returnspreview-stalewith the new hash and deletes nothing. A workflow that is turned on, or still being published, is refused withdeploy-refusedandrefusalCode: "workflow-live": turn it off first withset_workflow_stateand{ "kind": "deactivate" }, which needs Admin, then delete it. - Who may do what. Publishing a workflow follows the editor's rule: the person who created the version (or a platform admin). Deleting a workflow follows the app's rule too: only the person who created it (or a platform admin) can delete it; anyone else gets
refusalCode: "not-workflow-creator". Turning workflows on and off, and exposing them, needs a workspace admin. Creating, editing and publishing a form need a workspace admin somewhere in the account. These role rules apply on top of the tool's permission (Build to create and edit, Admin to publish): a connection needs both.
Build a form
An assistant can make a form, fill in its fields, and publish it, with the same four tools it uses for workflows, each called with type: "form":
create_workflow_draft({ type: "form", name: "Lead Intake" })makes a draft form with one starter field, exactly like New form in the app. It returns the form'sformId, its fields, itsformHashand the form builder link.propose_changes({ type: "form", formId, baseFormHash, changeSet })edits the draft. Every change set is checked against the same rules the form builder uses, all or nothing: if anything is wrong, nothing is saved. A malformed change set comes back asinvalid-argumentsnaming the field; an op that can't apply is reported on its own, so fix it and send again; problems with the finished form (its rules and its size) are listed together.preview_publish({ type: "form", formId })shows what going live means: anything still blocking it, the public link, and the fields added, removed or changed since it was last published.publish({ type: "form", formId, previewHash })makes the public page start taking submissions and returnsoutcome: "published". It is refused withpreview-staleif the form, or anything blocking it, changed since that preview.
A workflow then starts from the form with a Form Submission trigger whose formId is the new form. The workflow can't be published until the form is.
A form change set is a summary and a list of ops, applied in order. Fields are addressed by their key and pages by their number, as describe returns them; each op sees the form as the ops before it left it, so page numbers and positions shift as pages and fields are added or removed:
| Op | What it does |
|---|---|
add-field | Adds a field on a page, before position index (the end when left out). |
update-field | Changes a field's settings: set replaces each named setting whole, by the names add-field uses (to change one choice, resend the whole options list, built from the choices describe returns), and set.key renames the field; unset removes optional settings. A field's type can't change; remove it and add a new one. |
remove-field | Removes a field. |
move-field | Moves a field to another position on its own page. |
add-page | Adds an empty last page, with an optional title and description; the page that was last stops being the last step. |
update-page | Changes a page's title, description, whether it is the last step (terminal), and its branchRules: where to go next, by the number of another page. unset removes the title, the description or the rules. |
remove-page | Removes a page and its fields. A form always keeps one page. |
update-form | Changes the form's title, description or submitButtonLabel; unset removes the description. |
- Field types:
short_text,long_text,email,number,date,single_select,multi_select,checkbox,radio,file,signature,rating,address,phoneandhidden, each with the settings the form builder offers for it. A new or renamed field'skeyis snake_case: a lowercase letter, then lowercase letters, digits or underscores, at most 64 characters. A key built in the app in another shape still works for addressing the field. - Conditions: a field's
showIfand a page's branch rules are one comparison, such as{ "==": [{ "var": "budget" }, "over_20k"] }, or anand/orlist of up to 20 comparisons — the shape the form builder can show, so a person can still edit every rule. A comparison's value is text (up to 200 characters), a number,true,falseornull. A field'sshowIfcan't use the field itself, and a branch rule may also betrue(always go there). - Size: a change set can leave a form with at most 200 fields and 50 pages, so
describecan always show every field. A larger form built in the app can still be edited, as long as the change doesn't raise a count that is already over its limit. - Drafts only. An assistant edits draft forms only. A published form's page shows its saved fields straight away, so a person changes a live form in the form builder (or unpublishes it first). On a published form,
propose_changesanswersform-status-refusedwith a link to the form builder. - Reload an open form builder. The form builder saves the whole form. If you have the form open while an assistant edits it, reload the page before you change anything, or your save replaces the assistant's changes.
Check whether a workflow is live
describe({ id: "deployment:<workflowId>" }) answers for any workflow the connection reaches — published or not, exposed or not. It returns the active and last-active version ids, the newest version's status (for example pending-eval), whether it is exposed and under which slug, the setup steps still outstanding, and one entry per trigger with its phase:
| Phase | Meaning |
|---|---|
off | The workflow is not turned on. |
provisioning | Still being set up — a provider webhook registering, a phone number being pointed at the workflow, a schedule being created. |
needs_setup | Live, but waiting on a person — usually the webhook URL has not been registered with the sending system yet. The trigger's detail and the setupRequired steps say what to do. |
live | Ready to start runs. |
failed | Stopped and will not recover by itself — for example the connection was revoked. The detail names the fix. |
settled is true once no trigger is provisioning. While it is false, recheckAfterSeconds says when checking again is useful — poll until settled is true.
Read forms, tables, templates and app connection health
A connection that can author also reads the things a person builds with, so an assistant can wire a workflow to them without asking for every field key or column id:
- Forms:
find({ type: "form", query? })lists the account's forms (draft and published) with each one's slug and status.describe({ id: "form:<id>" })returns its fields:key(what a Form Submission trigger emits),label,type,required,page, a field's condition asshowIf, and for a dropdown, multi-select or radio field its option values asoptionsand each choice's label and value aschoices. It also returnspages(each page's number, title, field count, whether it is the last step, and its branch rules) andformHash, the base for editing it withpropose_changes(see Build a form). Up to 200 fields come back;omittedFieldCountsays how many more there are. - Tables:
find({ type: "table" })lists the tables this workspace can use: its own, and the account's.describe({ id: "table:<id>" })returns the columns (id, label, type, required, unique, indexed, and options on select columns), the primary key column and the time-to-live setting. - Templates:
find({ type: "template" })lists the account Library's templates: the agency's own (source: "account") and the built-in ones (source: "platform"). Templates other agencies share on the public gallery are not included.describe({ id: "template:<id>" })returns the latest revision's steps, the apps and app connections it needs, and its variables. - App connections:
find({ type: "connection" })lists every app connection this workspace can reach, broken ones first, each withstatus,healthyand a plainreason. An account connection appears only once it is shared with this workspace, and only to account admins, the same as on the Apps page.describe({ id: "connection:<id>" })adds token-refresh timing andhumanLink, the connection's page in the app, where a person reconnects it or finds where to re-enter the credential. It never returns a secret or a token.
reason says what happened, then what a person does next:
status | healthy | What happened |
|---|---|---|
active | true | Nothing: reason is null. |
pending | false | Setup was started but never finished. |
error | false | The last attempt to use or refresh this connection failed. |
reauth-required | false | For a sign-in (OAuth) connection: the sign-in expired. For any other kind: the saved credential stopped working. |
revoked | false | It was disconnected. |
| Credential kind | What a person does |
|---|---|
| Sign-in (OAuth) | Reconnect it on its connection page. |
| API key, basic, custom or DSN | Re-enter it from a node that uses it in the workflow editor. |
When healthy is false, show the person the reason and the humanLink.
Build and fill tables
With Build, an assistant can create a table, change its columns, and read and change its rows. Tables have no draft: every change is live as soon as it saves, and the table editor's own save rules decide what a change may do.
A typical exchange:
edit_table({ create: { name: "Leads" }, changes: [...] })adds Email (the primary key), Name (required) and Status (a dropdown of new, contacted and won, indexed). The answer is the table with each column's id.table_rows({ tableId, action: { kind: "insert", rows: [{ values: { "<Email column id>": "ada@example.com", ... } }] } })adds rows. Each row comes back with itsrowId— the Email, because Email is the primary key.table_rows({ tableId, action: { kind: "query", where: { columnId: "<Status column id>", equals: "new" } } })finds the rows still marked new.table_rows({ tableId, action: { kind: "update", rows: [{ rowId: "ada@example.com", values: { "<Status column id>": "contacted" } }] } })changes one.table_rows({ tableId, action: { kind: "delete", rowIds: ["bob@example.com"] } })answerspreview-requiredand lists the row. The assistant shows the person, then calls again withpreviewHashset todetails.freshHash, and the row is deleted.
Values are always keyed by column id, never by label, so renaming a column breaks nothing.
| Change | Undo | What it needs |
|---|---|---|
| Create a table | Delete it in the app (Delete table on the tables list) | Build |
| Add a column (optional) | Remove it (confirm step) | Build |
| Rename a column | Rename it back; ids and workflow references never change | Build |
| Retype a column | Retype it back: stored values are never rewritten, and the save rules refuse any change a stored value cannot survive | Build |
| Remove a column | None. Its values are never shown again, adding the column back does not restore them, and workflow steps that write it fail until changed | Build, then the confirm step |
| Insert rows | Delete them (confirm step) | Build |
| Update rows | None for the overwritten values, the same as a workflow's Update row step | Build |
| Delete rows | None | Build, then the confirm step |
How it behaves:
- The confirm step. Deleting rows or removing a column first answers
preview-requiredwithdetails.mustConfirm, one sentence per row or column that will be lost, and changes nothing. Only a second call carryingpreviewHashset todetails.freshHashgoes ahead. If a row or the table changed in between, the second call answerspreview-stalewith the new hash and changes nothing. - Filter on indexed columns only.
wherematches one indexed column's exact value. A column that is not indexed answersinvalid-argumentsnaming the indexed columns you can use; withoutwhere,querypages through every row (limitup to 50, default 20, andcursorfrom the previous page). A column can be indexed only while the table has no rows — withedit_table, or in the table editor. - Writes stop at the first failure. Rows are written one at a time, in order. The answer lists the rows written, the one that
failed(its index, code and message), and how many werenotAttempted. Nothing before the failure is undone, so fix the failed row and send it and the rest again. - Create-only settings.
requiredandprimaryKeycan be set only when creating a table. An indexed or unique column can be added or removed only while the table has no rows. - Account tables.
create: { scope: "account" }makes a table every workspace of the account can use. Creating or changing one, or writing its rows, needs an account admin or owner. - Archived tables can still be read with
query, but not changed.
Retired tools
These tools were removed. A client that still lists one can call it, and gets entitlement-denied (not retryable) with a message saying the tool was retired and a hint naming its replacement. A retired call is not counted against your budget. If the replacement is not in the assistant's tool list either, reconnect the connector to refresh it.
| Retired tool | Use instead |
|---|---|
list_workflows | find({ type: "workflow" }) (type draft for drafts) |
describe_workflow | describe({ id: "workflow:<slug>" }) for inputs, describe({ id: "deployment:<workflowId>" }) for deployment status |
get_run | describe({ id: "run:<runId>" }) |
list_runs | find({ type: "run" }) |
list_skills | find({ type: "skill" }) |
get_draft | describe({ id: "draft:<workflowId>" }) |
describe_node | describe({ id: "node:<canonical>" }) |
ground_apps | describe({ id: "app:<app slug>" }) for one app's nodes, find({ type: "node", query }) to search the catalog |
<prefix><workflow-slug> | run_workflow({ workflowSlug: "<workflow-slug>", input }) — the hint carries the slug |
A per-workflow tool name was your account's prefix (taskjuice_, or your own with a connector domain) followed by the workflow's slug, so taskjuice_send_invoice is answered with run_workflow({ workflowSlug: "send_invoice", … }).
Errors
Every tool failure is a structured result with a code, a message, a retryable flag, and usually a hint naming the fix.
| Code | Meaning | Retry? |
|---|---|---|
invalid-arguments | An argument failed validation; the message names the field. | Yes, corrected |
not-found | No such workflow, run, draft, skill, catalog app, form, table, template or app connection in this connection's scope. For a template, the hint adds that community templates from the public gallery cannot be read here; for an app connection, that an account connection appears only after it is shared with this workspace, and only to account admins. | No |
entitlement-denied | The tool isn't available to this connection (plan, API key, URL, or, for control_run, a client's own sign-in), or the connection lacks the tool's permission — then details.requiredPermission names it — or the tool was retired, and the hint names its replacement (see Retired tools). | No. If details.requiredPermission is set, reconnect (or use a new key) with that permission |
rate-limited | An hourly bound or the daily budget was hit. | Yes, after the hint's backoff |
run-refused | The platform declined to start the run (limits, safety, disabled workflow). | Not this session |
proposal-rejected | The change-set violated the workflow schema (every violation is listed), or a form change set broke the form builder's rules or the size limit (see Build a form). | Yes, corrected |
stale-draft | The draft or form changed since the hash you proposed against; details.freshHash is the current one. | Yes — re-read with describe and rebase |
compile-refused | validate_draft found a blocker, or propose_changes could not set up a trigger step (nothing was saved); the message names it. | Yes, after repairing the draft |
payload-too-large | Tool arguments exceeded 256 KB. | Yes, smaller |
invalid-input | run_workflow input didn't match the workflow's schema; the message names the field. | Yes, corrected |
preview-stale | The workflow, form, table or rows changed after the preview; details.freshHash is the current hash. | Yes — preview again, then retry |
preview-required | The change needs the person's agreement; details.mustConfirm lists what to show them, details.freshHash the hash to send. | Yes, with the hash once they agree |
internal-error | The tool failed on our side; the cause is never returned. | Yes, shortly |
deploy-refused | Publishing, the state change or the delete was refused; details.refusalCode says why and the hint says what to do. | Depends on the code — see below |
form-status-refused | The form is published (an assistant edits drafts only; the hint links the form builder) or archived (it can't be changed or published). | No |
run-control-refused | control_run could not act: details.refusalCode is terminal, awaiting-approval, state-changed or not-ready. | state-changed and not-ready yes; the others no |
step-test-refused | test_step could not test the step: details.refusalCode is not-testable, invalid-step, model-not-allowed, billing-plan-required, too-large, timed-out or execution-failed, and the hint says what to do. After timed-out the step may still finish, so check the target system before testing again. | |
table-refused | edit_table or table_rows was refused; details.refusalCode says why and the hint says what to do — see below. | Depends on the code — see below |
A missing permission comes back as entitlement-denied with a details object, so an assistant can explain it without parsing the message. Permission names in details are lowercase: read, run, build, admin.
{
"ok": false,
"code": "entitlement-denied",
"message": "publish needs the Admin permission, which this connection was not granted.",
"hint": "Ask the person to reconnect this assistant and tick Admin on the Authorize access screen. Call whoami to see what this connection can do.",
"retryable": false,
"details": { "requiredPermission": "admin", "grantedPermissions": ["read", "run", "build"] }
}For an API key the hint says instead that a key's permissions are fixed when it is created, and to ask for a new key. A tool the connection doesn't have at all answers Tool "<name>" is not available. with no details, the same as a name that doesn't exist (a retired name answers that it was retired instead). That covers a tool your plan, role, or credential doesn't include (API keys never reach Build or Admin tools). whoami says which of those applies.
deploy-refused codes an assistant can clear in the conversation (by changing the draft, waiting, or finishing setup steps) come back with retryable: true. Codes that need a person with another role, a plan change, or a reconnect come back false. One of those is mfa-step-up-required: your account requires two-factor authentication, and the assistant's connection was made before you verified it. Remove the TaskJuice connector in the assistant (Claude: Settings → Connectors; ChatGPT: Settings → Apps & Connectors), add it again, and complete the two-factor check when sign-in asks — then publish again.
table-refused codes:
refusalCode | When | What to do | Retry? |
|---|---|---|---|
archived | The table is archived, so it can't be changed. Its rows can still be read with query. | Tell the person the table is archived. Use another table, or create one with edit_table. | No |
not-permitted | The person's account role can't make this change (an account table needs an account admin or owner). | Tell the person; an account admin or owner has to make this change. Reconnecting won't help. | No |
plan-limit | Creating the table would pass the plan's table limit. | See details.planLimit. An account admin can upgrade the plan, or the person can delete a table they no longer use. | No |
schema-invalid | A change can't be applied, or the table editor's save rules refuse it; the message names the change. | Fix the change the message names and call again. describe the table to see its columns. | Yes |
cell-invalid | A value doesn't fit its column, names a column the table doesn't have, a required column is missing, or an update changes the row's primary key. | Fix the value to fit its column (describe the table for types and options) and call again. | Yes |
row-not-found | A row to delete doesn't exist (the message names up to five). | Query the table for its current row ids, then call again. | No |
page-too-large | A query page is over 256 KB. | Call again with a smaller limit. A row too large to return on its own can be seen in the app. | Yes |
duplicate-key | An inserted row's primary key already exists. | A row with that key exists. Query it and update it instead. | Yes |
unique-conflict | A value another row already holds in a unique column. | Another row holds that value. Use another value. | Yes |
row-limit | An insert would pass the plan's row limit. | Tell the person; they can delete rows or upgrade the plan. | No |
storage-frozen | The account is on the Free plan and out of storage, so new rows can't be saved (an insert or update). | Tell the person; the account owner can delete tables or agent memory they no longer need, or upgrade the plan. | No |
row-limit, storage-frozen, duplicate-key and unique-conflict only ever arrive as a write answer's failed.code, never as a refused call, and so can cell-invalid (one row of an insert or update) and row-not-found (one row of an update): the rows before the failed one are saved.
Limits
Tool calls are counted per account per day, shared across every connection and key — adding credentials never adds budget. Stopping or resuming a run with control_run is never counted against it, so an account that has used up its day can still stop runs.
| Plan | Tool calls per day |
|---|---|
| Free | 50 |
| Solo | 250 |
| Starter | 1,000 |
| Growth | 5,000 |
| Scale | 20,000 |
On top of the daily budget, hourly bounds protect against runaways: 10,000 calls per account, and per-credential class caps (3,000 reads, 600 runs and step tests, 600 authoring calls including edit_table and table_rows, 300 mapping previews, and 60 publish calls — preview_publish, publish, set_workflow_state and delete_workflow share that one budget). control_run has no class cap and is not counted against the daily budget or a key's daily call cap; only the 10,000-per-account bound, which every call shares, applies to it. A call refused for a missing permission still counts, against the daily budget and these hourly bounds in the refused tool's class (for control_run, the 10,000-per-account bound only), so an assistant retrying a refused tool is capped like any other. Form calls count in their tool's own class: creating and editing a form as authoring calls, previewing and publishing one as publish calls. find and describe are counted in the class of the type they read: drafts, nodes, apps, forms, tables, templates and app connections as authoring calls, everything else as reads. A find or describe call for a type this connection can't read is refused and still counts, in that type's class. A call to a tool that isn't available at all is not counted. An API key's optional daily call cap bounds that key's share of the account budget — it lowers what one key can spend, never raises the total.
Authoring availability by plan: Starter includes the always-available tools only; Growth and Scale include authoring.
A table_rows call reads or writes at most 50 rows, and a query page returns at most 256 KB.
Workflow runs started through MCP are normal runs — they appear in run history and count toward your plan's usage like any other run.
Next steps
- Connect an AI agent with MCP — setup, exposing workflows, API keys
- Usage and overage — how runs are metered and billed