Skip to content

Commit 7d9f1e9

Browse files
fix(console): the skill download teaches delete_record confirm: true and mirrors the served SKILL.md (#31015)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoLpvyeWbGwXcuZzD8qyUXpu)_ Fixes #30865 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Console 集成页「Download skill」下载的 SKILL.md 现在与 MCP 服务端下发的文本逐字一致:`delete_record` 带上 `confirm: true` 及拒收说明,并补上 `list_actions` / `run_action` / `resume_run` / `validate_expression` 和 OAuth 接入方式。 - 为什么改:服务端自 #30861 起拒绝不带 `confirm: true` 的删除(`ACTION_CONFIRMATION_REQUIRED`),照旧下载件操作的 AI 代理每次删除都会失败;下载件还漏教了业务动作工具。 - 风险与回滚:只改一个纯文本渲染函数和它的测试,不碰服务端;回滚即 revert 本 PR。 - 先看:`ui/apps/console/src/pages/developer/objectstack-skill.test.ts`(新的漂移钉:从源码导入服务端渲染函数逐字比对)、`objectstack-skill.ts` 的 Tools 段、changeset。 - 两分钟验证:`pnpm --dir ui exec vitest run apps/console/src/pages/developer/objectstack-skill.test.ts`,预期 13 个测试全绿。 - 需要你做的:无,席位落地。 - **席位意见**:ui 席 2 对照 head `828224c8` 的 diff 复核:3 个文件、+158 / −29,无受治理路径。控制台「下载技能」的 SKILL.md 改为与 MCP 服务端 `renderSkillMarkdown` 无 OAuth 范围时的文本逐字节一致(`delete_record` 带 `confirm: true` 及确认规则、补齐 `validate_expression`、`list_actions`/`run_action`/`resume_run`、OAuth 连接与 Bearer 密钥写法);测试在 node 环境按相对路径从源码导入服务端渲染器,五种参数逐字节比对,另钉住 11 个工具的顺序与删除调用形状,消融两侧各红。只改 `packages/mcp/src/skill-md.ts` 的 PR 不会触发这条比对(CI 过滤器缺口),已立卡 #31017。Clause-② no,不欠契约复审。同意落地。 ## What changes - `ui/apps/console/src/pages/developer/objectstack-skill.ts`: the mirror now renders exactly what `renderSkillMarkdown` (`packages/mcp/src/skill-md.ts`) serves to a caller with no OAuth scopes (an API key or a session), which is the data skill with no authoring section and no federation tools. It gains `OBJECTSTACK_MCP_SERVER_NAME` for the `claude mcp add` snippet. The header says what it mirrors and which test holds it. - `objectstack-skill.test.ts`: a **drift pin**. The test runs in the node environment, imports `renderSkillMarkdown` and the identity constants from source by relative path (the console does not depend on `@objectstack/mcp`, which is not browser-safe), and asserts byte equality for five option shapes (none, URL, env name, both padded, blank). Explicit pins sit beside it: the ordered tool list (11 tools) and the `delete_record` call shape with its confirmation sentence. The old "not Bearer" assertion is replaced by an x-api-key header assertion, because the server now teaches `Authorization: Bearer` as an accepted key form. - `.changeset/30865-console-skill-download-confirm.md`: `@object-ui/console` patch (the package is not private). No mirror text differs from the canonical render on purpose, so the pin is full-text equality rather than the tool-list fallback. ## Tests <details><summary>Measured drift before the fix (mirror vs canonical, same URL and env name, origin/main 8ae0ab9)</summary> `diff mirror-before.md canonical.md` with `{ mcpUrl: 'https://acme.objectos.app/api/v1/mcp', envName: 'Acme CRM' }`: 85 insertions, 16 deletions, 9 hunks. 1. Intro: canonical names business **actions** as tools and the OAuth consent-scope bound; the mirror said "runs as you" with data objects only. 2. When to use: canonical adds "run a business action" and "an action is always better than hand-editing". 3. Connect: canonical teaches two tracks, OAuth (with the `claude mcp add --transport http objectstack URL` example) and API key; the mirror taught the API key only. 4. Accepted headers: canonical accepts `Authorization: Bearer KEY` too; the mirror named only `ApiKey`. 5. Discover: canonical adds step 3, `list_actions`. 6. Tools: the mirror lacked `validate_expression`. 7. Tools: the mirror taught `delete_record({ objectName, recordId })` with no `confirm`; canonical teaches `delete_record({ objectName, recordId, confirm })` and the `ACTION_CONFIRMATION_REQUIRED` refusal. The mirror also lacked `list_actions`, `run_action` and `resume_run`. 8. Conventions: canonical adds "Prefer actions over hand-edits". 9. Recommended workflow: canonical adds `list_actions` in step 1 and `run_action` in step 4. Frontmatter (name, description) was already identical. After the fix: the five equality cases pass, so the diff is empty for every shape tested. </details> <details><summary>Test run (head 828224c)</summary> `pnpm --dir ui exec vitest run apps/console/src/pages/developer/objectstack-skill.test.ts` → Test Files 1 passed (1), Tests 13 passed (13). </details> <details><summary>Ablation (scripts/ablation-replace.mjs, both legs restored to the HEAD blob)</summary> - Leg 1, the confirm line: `- **delete_record({ objectName, recordId, confirm })** — delete a record,` → `- **delete_record({ objectName, recordId })** — delete a record,` in the mirror (anchor 1 → 0, blob 8612fda → 7cea2fc32802). Result: 6 failed, 7 passed. Red: "teaches delete_record with confirm: true, which the server requires" and all five "renders the canonical text for …" cases. Restored blob 8612fda == HEAD. - Leg 2, a canonical-side edit (shows the pin catches drift from either side): `- **Page large reads.** Use` → `- **Page large reads, always.** Use` in `packages/mcp/src/skill-md.ts`. Result: 5 failed, 8 passed (the five equality cases). Restored, `git diff HEAD` empty. No build sits between the test and either mutated file: both are imported from source. </details> ## Verification <details><summary>Local gates (derived with dispatch-gates.mjs --commands against this diff)</summary> Head `828224c88f`. 35 derived, 35 run, 0 NOT-MEASURED (`dispatch-gates.mjs --ran` reconciliation exit 0). Every line exit 0: ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/ci/snapshot-next.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 ``` Also `pnpm --dir ui exec vitest run apps/console/src/pages/developer/`: Test Files 20 passed (20), Tests 165 passed (165). `pnpm --dir ui exec vitest run scripts/`: Test Files 170 passed, 2 skipped; Tests 4991 passed, 2 skipped. </details> <details><summary>Console type-check (the relative source import brings packages/mcp/src/skill-md.ts into the console tsc program)</summary> `pnpm exec turbo run type-check --concurrency=2 --filter=@object-ui/console` → `@object-ui/console:type-check: cache miss, executing`, Tasks 73 successful, 73 total; lock VERDICT command-exit 0. Also `pnpm --dir ui run check:phantom-deps`, `check:self-import`, `check:unreferenced-sources`: exit 0 each. </details> ## Acceptance notes - **CI scoping blind spot (noted, not filed).** The drift pin lives in Test UI. A later PR that changes only `packages/mcp/src/skill-md.ts` matches neither the `ui` nor the `ui_consumers` filter in `ci.yml` (`packages/mcp/**` is not among the seven `ui_consumers` packages), so Test UI does not run on that PR or its queue entry; the pin goes red on the next `main` scheduled or dispatched full run instead. Closing it needs `packages/mcp/src/skill-md.ts` in the `ui` filter, which is root `ci.yml` and out of this card's bounds. Carrier: seat. - Open PRs checked for the serial rule: none touches `objectstack-skill.ts`, its test or `packages/mcp/src/skill-md.ts`. #30902 touches two other test files in `ui/apps/console/src/pages/developer/` (`consolePagesLocale-13460.test.tsx`, `flowRunsClientDiagnostic-25916.test.tsx`), with no overlap. <!-- signed: domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf --> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 584f7b8 commit 7d9f1e9

3 files changed

Lines changed: 158 additions & 29 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/console': patch
3+
---
4+
5+
fix(console): the "Download skill" SKILL.md teaches `delete_record` with `confirm: true`, and the rest of what the server serves
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The Integrations page's "Download skill" button wrote a SKILL.md that taught `delete_record({ objectName, recordId })` with no `confirm`. The MCP server refuses that call with `ACTION_CONFIRMATION_REQUIRED` and deletes nothing, so an agent that followed the downloaded skill failed every delete. The download now carries the same text the MCP server serves to an API key or a session: `delete_record({ objectName, recordId, confirm })` with the confirmation rule, the business-action tools (`list_actions`, `run_action`, `resume_run`), `validate_expression`, the OAuth connection track, and the `Authorization: Bearer` key form. A test compares the download with the server's renderer byte for byte, so the two cannot drift apart again unnoticed.

‎ui/apps/console/src/pages/developer/objectstack-skill.test.ts‎

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
// @vitest-environment node
12
import { describe, it, expect } from 'vitest';
23

4+
// The canonical renderer, imported from source by relative path: the console
5+
// does not depend on `@objectstack/mcp` (it is not browser-safe), and the
6+
// comparison below is the whole point of this file.
7+
import {
8+
renderSkillMarkdown,
9+
OBJECTSTACK_MCP_SERVER_NAME as CANONICAL_MCP_SERVER_NAME,
10+
OBJECTSTACK_SKILL_DESCRIPTION as CANONICAL_SKILL_DESCRIPTION,
11+
OBJECTSTACK_SKILL_NAME as CANONICAL_SKILL_NAME,
12+
} from '../../../../../../packages/mcp/src/skill-md';
313
import {
414
renderObjectStackSkill,
15+
OBJECTSTACK_MCP_SERVER_NAME,
516
OBJECTSTACK_SKILL_NAME,
617
OBJECTSTACK_SKILL_DESCRIPTION,
718
} from './objectstack-skill';
@@ -17,6 +28,12 @@ function frontmatter(md: string): Record<string, string> {
1728
return out;
1829
}
1930

31+
/** The tool names the skill's `## Tools` section teaches, in order. */
32+
function toolNames(md: string): string[] {
33+
const section = md.split('\n## Tools\n')[1]?.split('\n## ')[0] ?? '';
34+
return [...section.matchAll(/^- \*\*([a-z_]+)\(/gm)].map((m) => m[1]);
35+
}
36+
2037
describe('renderObjectStackSkill', () => {
2138
it('emits valid SKILL.md frontmatter (name + description)', () => {
2239
const fm = frontmatter(renderObjectStackSkill());
@@ -34,29 +51,56 @@ describe('renderObjectStackSkill', () => {
3451
expect(renderObjectStackSkill()).toContain('<YOUR_ENV_MCP_URL>');
3552
});
3653

37-
it('documents x-api-key auth, not Bearer', () => {
38-
const md = renderObjectStackSkill();
39-
expect(md).toContain('x-api-key');
40-
expect(md).not.toMatch(/Authorization:\s*Bearer/);
54+
it('documents the x-api-key header for headless clients', () => {
55+
expect(renderObjectStackSkill()).toContain('x-api-key: <YOUR_API_KEY>');
4156
});
4257

43-
it('lists all object-CRUD tools and instructs live discovery', () => {
58+
it('lists every data and action tool and instructs live discovery', () => {
4459
const md = renderObjectStackSkill();
45-
for (const t of [
60+
expect(toolNames(md)).toEqual([
4661
'list_objects',
4762
'describe_object',
63+
'validate_expression',
4864
'query_records',
4965
'get_record',
5066
'create_record',
5167
'update_record',
5268
'delete_record',
53-
]) {
54-
expect(md).toContain(t);
55-
}
69+
'list_actions',
70+
'run_action',
71+
'resume_run',
72+
]);
5673
expect(md).toContain('discovered live');
5774
});
5875

76+
it('teaches delete_record with confirm: true, which the server requires', () => {
77+
const md = renderObjectStackSkill();
78+
expect(md).toContain('**delete_record({ objectName, recordId, confirm })**');
79+
expect(md).toContain('The server REFUSES a call without `confirm: true`');
80+
expect(md).not.toContain('delete_record({ objectName, recordId })');
81+
});
82+
5983
it('includes the env name when provided', () => {
6084
expect(renderObjectStackSkill({ envName: 'Acme CRM' })).toContain('**Acme CRM**');
6185
});
6286
});
87+
88+
// Drift pin: the download must be the SKILL.md the server serves to a caller
89+
// with no OAuth scopes (an API key or a session), byte for byte.
90+
describe('renderObjectStackSkill mirrors renderSkillMarkdown', () => {
91+
it('shares the canonical identity constants', () => {
92+
expect(OBJECTSTACK_SKILL_NAME).toBe(CANONICAL_SKILL_NAME);
93+
expect(OBJECTSTACK_SKILL_DESCRIPTION).toBe(CANONICAL_SKILL_DESCRIPTION);
94+
expect(OBJECTSTACK_MCP_SERVER_NAME).toBe(CANONICAL_MCP_SERVER_NAME);
95+
});
96+
97+
it.each([
98+
['no options', {}],
99+
['an MCP URL', { mcpUrl: 'https://acme.objectos.app/api/v1/mcp' }],
100+
['an env name', { envName: 'Acme CRM' }],
101+
['both, padded with whitespace', { mcpUrl: ' https://acme.objectos.app/api/v1/mcp ', envName: ' Acme CRM ' }],
102+
['blank values', { mcpUrl: ' ', envName: '' }],
103+
])('renders the canonical text for %s', (_label, options) => {
104+
expect(renderObjectStackSkill(options)).toBe(renderSkillMarkdown(options));
105+
});
106+
});

‎ui/apps/console/src/pages/developer/objectstack-skill.ts‎

Lines changed: 95 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
* objectstack-skill — client-side renderer for the portable ObjectStack Agent
33
* Skill (`SKILL.md`), used by the Integrations page "Download skill" button.
44
*
5-
* CANONICAL SOURCE: `renderSkillMarkdown` in `@objectstack/mcp` (framework).
6-
* That package pulls in the MCP SDK and is not browser-friendly to import here,
7-
* so this is a faithful mirror for the download UX. Keep the two in sync — the
8-
* unit test asserts the structural invariants (frontmatter, URL slot, tools).
5+
* CANONICAL SOURCE: `renderSkillMarkdown` in `@objectstack/mcp`
6+
* (`packages/mcp/src/skill-md.ts`). That package pulls in the MCP SDK and is
7+
* not browser-friendly to import here, so this is a mirror of what the server
8+
* serves to a caller with no OAuth scopes (an API key or a session): the data
9+
* skill, with no authoring section and no federation tools. It must stay
10+
* byte-identical to that render — `objectstack-skill.test.ts` imports the
11+
* canonical renderer from source and compares the two, so an edit to either
12+
* side without the other goes red there.
913
*
1014
* Per ADR-0036 Amendment C this is ONE generic skill: it never enumerates a
1115
* tenant's schema (the agent discovers live via list_objects/describe_object);
@@ -20,6 +24,8 @@ export interface RenderSkillOptions {
2024
}
2125

2226
export const OBJECTSTACK_SKILL_NAME = 'objectstack';
27+
/** The server name the `claude mcp add` snippet registers (mirrors `OBJECTSTACK_MCP_SERVER_NAME`). */
28+
export const OBJECTSTACK_MCP_SERVER_NAME = 'objectstack';
2329
export const OBJECTSTACK_SKILL_DESCRIPTION =
2430
'Query and modify data in an ObjectStack app over MCP — discover objects, ' +
2531
'read and filter records, and create/update/delete under your own ' +
@@ -42,18 +48,23 @@ description: ${OBJECTSTACK_SKILL_DESCRIPTION}
4248
4349
# ObjectStack
4450
45-
${intro} An ObjectStack environment exposes its data **objects** (tables) as
46-
tools over the Model Context Protocol (MCP). Every operation runs **as you** —
47-
under your account's permissions and row-level security — so you may see a
48-
subset of rows, or get a permission error on a write. That is expected
51+
${intro} An ObjectStack environment exposes its data **objects** (tables) and
52+
its business **actions** (registered app logic: approvals, conversions, flow
53+
triggers) as tools over the Model Context Protocol (MCP). Every operation runs
54+
under the permissions and row-level security of the identity that connected you
55+
— and, for an OAuth connection, **within the consent scopes you were granted**
56+
(a read-only scope can never write, even where that user could). So you may see
57+
a subset of rows, or get a permission error on a write. That is expected
4958
governance, not a failure.
5059
5160
## When to use
5261
5362
Use these tools whenever the user wants to **inspect or change data** in their
54-
ObjectStack app: look up records, filter/report, create or update entries, or
55-
clean up data. Prefer these tools over guessing — the environment is the source
56-
of truth.
63+
ObjectStack app — look up records, filter/report, create or update entries,
64+
clean up data — or **run a business action** the app defines (approve a
65+
request, convert a lead, kick off a flow). Prefer these tools over guessing —
66+
the environment is the source of truth, and an action is always better than
67+
hand-editing the records it would have touched.
5768
5869
## Connect
5970
@@ -63,15 +74,32 @@ This skill drives the MCP server at:
6374
${url}
6475
\`\`\`
6576
66-
Authenticate with an ObjectStack API key sent as a request header (the key is
67-
shown to you once when created; treat it like a password):
77+
Two authentication tracks are supported:
78+
79+
**OAuth (recommended for interactive clients).** Add the URL as a remote MCP
80+
server with no credentials; the deployment is its own OAuth 2.1 authorization
81+
server, so an OAuth-capable client (claude.ai custom connectors, Claude
82+
Desktop, Claude Code) discovers it automatically, registers itself, and opens
83+
a browser login. You authorize the connection in the browser; the client then
84+
acts as an agent **on your behalf** — every tool call is bounded by the
85+
intersection of the consent scopes you grant and your own permissions/RLS (it
86+
can never exceed either). Example (Claude Code):
87+
88+
\`\`\`
89+
claude mcp add --transport http ${OBJECTSTACK_MCP_SERVER_NAME} ${url}
90+
\`\`\`
91+
92+
**API key (headless: CI, scripts, agents without a browser).** Send an
93+
ObjectStack API key as a request header (the key is shown to you once when
94+
created; treat it like a password):
6895
6996
\`\`\`
7097
x-api-key: <YOUR_API_KEY>
7198
\`\`\`
7299
73-
(The header \`Authorization: ApiKey <YOUR_API_KEY>\` is also accepted.) If your
74-
MCP client supports custom headers on a remote server, set the header there.
100+
(\`Authorization: ApiKey <YOUR_API_KEY>\` and \`Authorization: Bearer
101+
<YOUR_API_KEY>\` are also accepted.) If your MCP client supports custom
102+
headers on a remote server, set the header there.
75103
76104
## Discover before you act
77105
@@ -81,6 +109,8 @@ always current even as the app evolves:
81109
1. \`list_objects\` — see what objects exist.
82110
2. \`describe_object({ objectName })\` — get an object's fields (name, type,
83111
required) before querying or writing it.
112+
3. \`list_actions\` — see what business actions you may run (each entry
113+
includes its parameters and whether it needs a \`recordId\`).
84114
85115
Always discover the relevant object's shape before constructing a filter or a
86116
create/update payload.
@@ -89,14 +119,52 @@ create/update payload.
89119
90120
- **list_objects()** — list available objects (system \`sys_*\` objects are hidden).
91121
- **describe_object({ objectName })** — an object's fields and features.
122+
- **validate_expression({ objectName, expression, site?, severity?, triggerKind?, sweepObject? })** — check a CEL
123+
expression against an object's schema *before* you author it into a formula,
124+
validation, or flow condition. Returns build-time errors (bare field refs,
125+
unknown fields/functions) and advisory warnings (text/boolean fields misused
126+
in arithmetic; an error at a \`validation\` site, worded for the rule's
127+
\`severity\`, and at a \`flow_condition\` of a record-triggered or
128+
time-relative flow, where it fails the run; a warning on any other flow), plus the fields and
129+
stdlib functions in scope. \`site\` is \`formula\` | \`validation\` |
130+
\`flow_condition\` | \`template\` (default \`formula\`). Pass the rule's
131+
\`severity\` or the flow's \`triggerKind\` (\`record_change\` by default) to
132+
get the verdict the publish gate gives that rule or flow; for a
133+
\`time_relative\` flow, pass the object its sweep scans (\`timeRelative.object\`)
134+
as \`objectName\`: its conditions read rows of that object, and a start whose
135+
\`objectName\` names another object is refused at publish
136+
(\`flow-time-relative-object-mismatch\`). \`sweepObject\` names that same object.
92137
- **query_records({ objectName, where?, fields?, limit?, offset?, orderBy? })** —
93138
read records. \`where\` is a field→value match, e.g. \`{ "status": "open" }\`.
94139
Results are page-capped; use \`limit\`/\`offset\` to page.
95140
- **get_record({ objectName, recordId })** — fetch one record by id.
96141
- **create_record({ objectName, data })** — create a record.
97142
- **update_record({ objectName, recordId, data })** — change fields on a record.
98-
- **delete_record({ objectName, recordId })** — delete a record (destructive —
99-
confirm with the user first).
143+
- **delete_record({ objectName, recordId, confirm })** — delete a record,
144+
permanently. The server REFUSES a call without \`confirm: true\`
145+
(\`ACTION_CONFIRMATION_REQUIRED\`) and nothing is deleted. Ask your human
146+
first; the flag asserts an approval, it does not obtain one.
147+
- **list_actions()** — list the business actions available to you (only those
148+
the app author exposed to AI and you are permitted to run), with each
149+
action's declared parameters, whether it operates on a record, and whether
150+
it is flagged destructive.
151+
- **run_action({ actionName, objectName?, recordId?, params?, confirm? })** —
152+
invoke a business action by name. Invocation is permission-gated, but the
153+
action body executes the app's registered logic as trusted code (it can
154+
mutate data or trigger flows with the app's own authority). Pass
155+
\`recordId\` for record-scoped actions and \`params\` for declared inputs;
156+
\`objectName\` only disambiguates a name shared by multiple objects. Pass
157+
\`confirm: true\` on an action \`list_actions\` reports as
158+
\`requiresConfirmation\` — the server REFUSES such a call without it
159+
(\`ACTION_CONFIRMATION_REQUIRED\`) and nothing runs. Ask your human first;
160+
the flag asserts an approval, it does not obtain one.
161+
- **resume_run({ runId, values?, confirm? })** — continue a flow run that
162+
\`run_action\` answered with \`status: "paused"\` and a \`screen\` (a form the
163+
flow needs filled in). Send the screen's field values in \`values\`, keyed by
164+
the names in \`screen.fields\`. The run completes, or pauses again on its next
165+
screen, which you fill with another \`resume_run\`. Only the user whose call
166+
started the run can resume it, under the same gates as \`run_action\`
167+
(including \`confirm: true\` for an action flagged \`requiresConfirmation\`).
100168
101169
## Conventions & gotchas
102170
@@ -108,13 +176,20 @@ create/update payload.
108176
- **Writes are real and immediate.** There is no implicit dry-run. Confirm
109177
destructive actions (\`delete_record\`, bulk updates) with the user.
110178
- **Page large reads.** Use \`limit\`/\`offset\` rather than asking for everything.
179+
- **Prefer actions over hand-edits.** When \`list_actions\` offers an action for
180+
the task (approve, convert, close, …), call it instead of updating the
181+
records yourself — actions carry the app's validation and side effects.
182+
Confirm first when an action is record-destructive or flagged for
183+
confirmation.
111184
112185
## Recommended workflow
113186
114-
1. \`list_objects\` to orient.
187+
1. \`list_objects\` (and \`list_actions\` when the task sounds like a business
188+
operation) to orient.
115189
2. \`describe_object\` on the target object.
116190
3. \`query_records\` to read / verify current state.
117-
4. \`create_record\` / \`update_record\` / \`delete_record\` to make changes,
118-
confirming destructive steps with the user.
191+
4. \`run_action\` when a matching business action exists; otherwise
192+
\`create_record\` / \`update_record\` / \`delete_record\` — confirming
193+
destructive steps with the user.
119194
`;
120195
}

0 commit comments

Comments
 (0)