From f2cd1d931c8e90319c1dac010657a26764553428 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 14:40:19 +0000 Subject: [PATCH 1/3] docs(skills,automation): a connector_action input is judged against the action's inputSchema before dispatch After #27995 (PR #28035) the evaluated connectorConfig.input is checked against the action's declared inputSchema (required, type with OpenAPI nullable, properties, items) and a breach fails the step as VALIDATION_ERROR, fault-routable, with nothing sent. The automation skill's connector_action row and the integration recipes still taught the input as sent unchecked, and the connectors docs page had no failure-modes row for the refusal. - SKILL.md: the connector_action row says what is judged, the code, that a fault edge routes it and that nothing is sent; os validate still does not judge the input (#27968). - integration-recipes.md: the same, plus null-on-optional reads as absent and the int() cast for a string-held id. - connectors.mdx: a failure-modes row with the code and the remedy. The new text is paid for by same-file deletion of restatement, so both skill files stay within their token ceilings without a ratchet change. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cwp1Wc6nB6ePmS4ePn --- content/docs/automation/connectors.mdx | 1 + skills/objectstack-automation/SKILL.md | 23 ++++++++-------- .../references/integration-recipes.md | 26 +++++++++++-------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/content/docs/automation/connectors.mdx b/content/docs/automation/connectors.mdx index 396c2d5b91a..10dba9a5e8e 100644 --- a/content/docs/automation/connectors.mdx +++ b/content/docs/automation/connectors.mdx @@ -456,6 +456,7 @@ retry): | MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) | **Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic | | A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer) | **Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it | | Wrong spec URL (other 4xx) or unparseable document | Configuration fault — **hard boot error** | +| A `connector_action` input that, once evaluated, breaks the action's `inputSchema` (a missing `required` parameter, a wrong `type`) | **The step fails before dispatch** with `VALIDATION_ERROR`, naming each parameter path, and nothing is sent; a `fault` edge or `try_catch` can route it. Supply the parameter or send the declared type: text is never converted, so cast it in a CEL envelope (`int()`, `double()`, `bool()`). `GET /api/v1/automation/connectors` lists the schema; `os validate` does not judge the input yet | | A bad entry arrives via **runtime reload** (a Studio publish, a direct active save through `PUT /api/v1/meta/connector/:name`, or a delete, made on any replica — peers reconcile on [cluster lane 2](/docs/kernel/cluster#62-the-cluster-invalidation-family); or a dev reload) | Logged and **skipped**, never crashing the live server; a changed instance's old connector keeps serving until its replacement materializes | ## Catalog descriptors: entries without a `provider` diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 58280edfa64..f4522e5f8a3 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -85,7 +85,7 @@ Flows are built from **built-in node types** (plugins register more, e.g. |:-----|:--------| | `http` | Call an external HTTP API — always set `timeoutMs` (unset = no timeout). `url` and `headers` are served as written with the definition to every member who can read flows — only `signingSecret` and a start node's `secret` are withheld — so never a credential there: a credential means a declared connector called via `connector_action` | | `notify` | Send a notification through the messaging service (inbox by default) | -| `connector_action` | Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow` `input`; `os validate` does not judge it yet, so a malformed envelope fails at run time | +| `connector_action` | Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow` `input`, judged once evaluated against the action's `inputSchema` (`required`, `type`, `properties`, `items`) before dispatch: a breach fails the step `VALIDATION_ERROR` (`fault`-routable), nothing sent. `os validate` does not judge it yet | | `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) | | `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen. `'record_link'` (never `'record'`, an object field type) takes no input — it shows the `reference` record whose id `defaultValue` evaluates to, by display name, as a link to its record page: the result screen of a flow that created records, `{ name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }` over a `create_record` `outputVariable: 'account'`. Both keys required; `required` / `options` / `placeholder` / `min` / `max` refused; a resume value under its name refused (`400 INVALID_SCREEN_INPUT`). A completed `end` `message` is refused and a markdown link in a screen `description` renders literally. Next / Submit and "Step N of M" are not authored: the engine reads the graph when the screen pauses and serves `progress` (`nextScreen: 'none' | 'possible' | 'certain'`; `step` / `stepCount` only when every path agrees), which alone labels the button and draws the counter. A `decision`, `fault` edge, loop back, non-screen pause (`approval`, `wait`) or `subflow` after a screen answers `possible` — "Next", no count. `progress` in a node `config` is refused; never hand-roll a button label or step counter into `fields` | @@ -93,7 +93,7 @@ Flows are built from **built-in node types** (plugins register more, e.g. | Node | Purpose | |:-----|:--------| -| `approval` | Route a record for human sign-off — **suspends** the run until a decision, then continues down the `approve` / `reject` branch (from `plugin-approvals`) | +| `approval` | Route a record for human sign-off — **suspends** the run until a decision, then continues down the `approve` / `reject` branch | ### `notify` — the most-used node type @@ -193,13 +193,13 @@ type: 'text', isInput: true }, { name: 'approval_result', type: 'boolean', isOut A whole-string **query placeholder** (`{current_user_id}`, `{today}`, `{current_year_start}`) passes through verbatim for the query engine; a flow variable never shadows it, and only `filter` gets this hand-off. -- **An operand that evaluates to `null` refuses the node** (not - `fault`-routable): a dropped condition does not narrow a query, it widens - it, so a routed `delete_record` would empty the object while the run reports - success. Where the value may be absent, route around the node with a - `decision`; to match an empty field, write the literal `null`. A relation - hop (`record.owner.email`) needs the start node's `config.expand` to list - the field — else the fault names the missing hop, not the remedy. +- **An operand that evaluates to `null` refuses the node**: a dropped condition + does not narrow a query, it widens it, so a routed `delete_record` would empty + the object while the run reports success. Where the value may be absent, + route around the node with a `decision`; to match an empty field, write the + literal `null`. A relation hop (`record.owner.email`) needs the start node's + `config.expand` to list the field — else the fault names the missing hop, not + the remedy. --- @@ -244,8 +244,7 @@ write them right the first time: 3. **`script` nodes call a registered function — nothing else.** `config.function` (**required**: an empty node refuses at execute, an unregistered name fails - loudly) names one registered via `defineStack({ functions: { my_fn: (ctx) => … } })`; - `notify` delivers, `connector_action` reaches a vendor, a function computes. + loudly) names one registered via `defineStack({ functions: { my_fn: (ctx) => … } })`. **A flow `function` is a PURE compute step — it does NOT read/write the database.** It receives `ctx.input` and **returns** a value; `config.outputVariable` @@ -408,7 +407,7 @@ A vendor system reachable over HTTP (OpenAPI, remote MCP, plain REST) is a `connectors:` entry inside the app — `provider: 'openapi' | 'mcp' | 'rest'`, a packaged spec, `auth.credentialRef` (the customer's administrator supplies the value) — dispatched by `connector_action`; an inbound vendor callback is an -`api` flow. Never a plugin, never an `http` node holding a credential. Recipes, +`api` flow. Never a plugin. Recipes, credential hand-off, loader limits: [references/integration-recipes.md](./references/integration-recipes.md). Platform code only when one holds — and then it is the platform's, filed as a card, the app still writing none: the protocol is not HTTP; the work writes diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index 2b139946076..3c067ef5617 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -16,9 +16,14 @@ deployment and to the sender. Scaffolded apps list the three executors in - Actions come from the upstream (an OpenAPI `operationId`, an MCP tool name, the `rest` provider's single `request`) — authored on the entry, they are refused. - `connectorConfig.input` is a value-slot map like `subflow` `input`: a top-level - CEL envelope is evaluated, any other value sent as written (`{…}`, `{{ }}` are - literal text) — `input: { customer: { dialect: 'cel', source: 'record.stripe_id' } }`. - `os validate` does not judge it yet, so a malformed envelope fails at run time. + CEL envelope is evaluated, any other value kept as written (`{…}`, `{{ }}` are + literal text). Before dispatch the result is judged against the action's + `inputSchema`: `required`, `type` (with OpenAPI `nullable`), `properties`, + `items`; `null` on an optional parameter is absent. A breach fails the step + `VALIDATION_ERROR` (`fault`-routable) naming each path, nothing sent. Text + is never converted: cast a string-held id, + `path: { dialect: 'cel', source: '{"petId": int(record.vendor_id)}' }`. + `os validate` does not judge it yet. - The answer lands as `.` (`rest` / `openapi`: `{ status, ok, body }`; `mcp`: `{ ok, content, structuredContent? }`), read from a later node's value slot: `{ dialect: 'cel', source: 'bal.body.available[0].amount' }`. @@ -63,7 +68,7 @@ dereference every `$ref` offline, in-document ones included: the loader skips a `$ref` parameter, request body or response, and the derived input/output schema silently loses it. `providerConfig.include` allowlists operation keys as the `mcp` provider's does (absent = every operation; a non-array or unknown entry is -fatal); trimming still keeps the package small. +fatal). ## 2 · Remote MCP server, static header — GitHub @@ -103,10 +108,10 @@ export const WarehouseApi = defineConnector({ ```ts export const CarrierCallback = defineFlow({ name: 'carrier_callback', label: 'Carrier status callback', type: 'api', - runAs: 'system', // an inbound post carries no trigger user + runAs: 'system', nodes: [ { id: 'start', type: 'start', label: 'POST …/hooks/carrier_callback/carrier', - config: { hookId: 'carrier', secretRef: 'CARRIER_HOOK_SECRET' } }, // never a literal secret + config: { hookId: 'carrier', secretRef: 'CARRIER_HOOK_SECRET' } }, { id: 'apply', type: 'update_record', label: 'Apply status', config: { objectName: 'shipment', filter: { tracking_no: { dialect: 'cel', source: 'record.tracking_no' } }, fields: { status: { dialect: 'cel', source: 'record.status' } } } }, @@ -127,7 +132,7 @@ name the gap and poll its status (recipe 1's shape) until vendor schemes verify. ```ts export const LegacyErp = defineConnector({ name: 'legacy_erp', label: 'Legacy ERP', type: 'api', provider: 'openapi', - providerConfig: { spec: { // inline OpenAPI 3.x + providerConfig: { spec: { openapi: '3.0.3', info: { title: 'Legacy ERP', version: '1' }, servers: [{ url: 'https://erp.example.com' }], paths: { '/v2/open-orders': { get: { operationId: 'listOpenOrders', @@ -138,16 +143,15 @@ export const LegacyErp = defineConnector({ // connector_action { connectorId: 'legacy_erp', actionId: 'listOpenOrders', input: {} } ``` -Each `operationId` is a named action with its input schema, visible in the flow -palette and to agents; `provider: 'rest'` instead when the path is per call. +Each `operationId` is a named action in the flow palette and to agents; +`provider: 'rest'` instead when the path is per call. ## Not yet — declare the rest, mark the integration blocked, write no plugin - **OAuth-protected vendor** (DocuSign, Salesforce, Google): the declarative `auth` union has no `oauth2`; the authorization-code / refresh lifecycle is not delivered in the open tier. Do not park a short-lived access - token behind a `bearer` ref. Declare the connector, fields and flows, mark the - integration **blocked** on the platform's outbound-OAuth card, and say so. + token behind a `bearer` ref. Blocked on the platform's outbound-OAuth card. - **Signature-authenticated vendor** (Alipay, WeChat Pay, Aliyun — RSA2 / HMAC canonical-request signing): no auth kind signs requests today; same handling, blocked on the request-signing card. From e7e87755eab4a82c0037890a8f1d16a8acfbe5a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:24:44 +0000 Subject: [PATCH 2/3] docs(skills,automation): name the check os validate does not perform yet Contract review 6526392518 must-fix 1: "os validate does not judge it yet" left its antecedent open, and the open PR for #27968 makes registration and os validate judge the value-slot envelope (not the schema fit). All three sites now say os validate does not judge the input against the action's inputSchema, true in either landing order. Paid in integration-recipes.md by recipe 1's "holds GetBalance alone" comment: the comment above it says the document is trimmed to the operations the app calls, and the flow below calls GetBalance. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cwp1Wc6nB6ePmS4ePn --- content/docs/automation/connectors.mdx | 2 +- skills/objectstack-automation/SKILL.md | 2 +- .../objectstack-automation/references/integration-recipes.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content/docs/automation/connectors.mdx b/content/docs/automation/connectors.mdx index 10dba9a5e8e..7d36bfea333 100644 --- a/content/docs/automation/connectors.mdx +++ b/content/docs/automation/connectors.mdx @@ -456,7 +456,7 @@ retry): | MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) | **Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic | | A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer) | **Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it | | Wrong spec URL (other 4xx) or unparseable document | Configuration fault — **hard boot error** | -| A `connector_action` input that, once evaluated, breaks the action's `inputSchema` (a missing `required` parameter, a wrong `type`) | **The step fails before dispatch** with `VALIDATION_ERROR`, naming each parameter path, and nothing is sent; a `fault` edge or `try_catch` can route it. Supply the parameter or send the declared type: text is never converted, so cast it in a CEL envelope (`int()`, `double()`, `bool()`). `GET /api/v1/automation/connectors` lists the schema; `os validate` does not judge the input yet | +| A `connector_action` input that, once evaluated, breaks the action's `inputSchema` (a missing `required` parameter, a wrong `type`) | **The step fails before dispatch** with `VALIDATION_ERROR`, naming each parameter path, and nothing is sent; a `fault` edge or `try_catch` can route it. Supply the parameter or send the declared type: text is never converted, so cast it in a CEL envelope (`int()`, `double()`, `bool()`). `GET /api/v1/automation/connectors` lists the schema; `os validate` does not judge the input against the `inputSchema` yet | | A bad entry arrives via **runtime reload** (a Studio publish, a direct active save through `PUT /api/v1/meta/connector/:name`, or a delete, made on any replica — peers reconcile on [cluster lane 2](/docs/kernel/cluster#62-the-cluster-invalidation-family); or a dev reload) | Logged and **skipped**, never crashing the live server; a changed instance's old connector keeps serving until its replacement materializes | ## Catalog descriptors: entries without a `provider` diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index f4522e5f8a3..173dba935a6 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -85,7 +85,7 @@ Flows are built from **built-in node types** (plugins register more, e.g. |:-----|:--------| | `http` | Call an external HTTP API — always set `timeoutMs` (unset = no timeout). `url` and `headers` are served as written with the definition to every member who can read flows — only `signingSecret` and a start node's `secret` are withheld — so never a credential there: a credential means a declared connector called via `connector_action` | | `notify` | Send a notification through the messaging service (inbox by default) | -| `connector_action` | Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow` `input`, judged once evaluated against the action's `inputSchema` (`required`, `type`, `properties`, `items`) before dispatch: a breach fails the step `VALIDATION_ERROR` (`fault`-routable), nothing sent. `os validate` does not judge it yet | +| `connector_action` | Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow` `input`, judged once evaluated against the action's `inputSchema` (`required`, `type`, `properties`, `items`) before dispatch: a breach fails the step `VALIDATION_ERROR` (`fault`-routable), nothing sent. `os validate` does not judge the `inputSchema` fit yet | | `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) | | `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen. `'record_link'` (never `'record'`, an object field type) takes no input — it shows the `reference` record whose id `defaultValue` evaluates to, by display name, as a link to its record page: the result screen of a flow that created records, `{ name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }` over a `create_record` `outputVariable: 'account'`. Both keys required; `required` / `options` / `placeholder` / `min` / `max` refused; a resume value under its name refused (`400 INVALID_SCREEN_INPUT`). A completed `end` `message` is refused and a markdown link in a screen `description` renders literally. Next / Submit and "Step N of M" are not authored: the engine reads the graph when the screen pauses and serves `progress` (`nextScreen: 'none' | 'possible' | 'certain'`; `step` / `stepCount` only when every path agrees), which alone labels the button and draws the counter. A `decision`, `fault` edge, loop back, non-screen pause (`approval`, `wait`) or `subflow` after a screen answers `possible` — "Next", no count. `progress` in a node `config` is refused; never hand-roll a button label or step counter into `fields` | diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index 3c067ef5617..6ef3f927113 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -23,7 +23,7 @@ deployment and to the sender. Scaffolded apps list the three executors in `VALIDATION_ERROR` (`fault`-routable) naming each path, nothing sent. Text is never converted: cast a string-held id, `path: { dialect: 'cel', source: '{"petId": int(record.vendor_id)}' }`. - `os validate` does not judge it yet. + `os validate` does not judge the `inputSchema` fit yet. - The answer lands as `.` (`rest` / `openapi`: `{ status, ok, body }`; `mcp`: `{ ok, content, structuredContent? }`), read from a later node's value slot: `{ dialect: 'cel', source: 'bal.body.available[0].amount' }`. @@ -40,7 +40,7 @@ deployment and to the sender. Scaffolded apps list the three executors in // it in the package, package-relative. export const StripeConnector = defineConnector({ name: 'stripe', label: 'Stripe', type: 'api', provider: 'openapi', - providerConfig: { spec: './src/connectors/stripe.trimmed.json' }, // holds `GetBalance` alone + providerConfig: { spec: './src/connectors/stripe.trimmed.json' }, auth: { type: 'bearer', credentialRef: 'STRIPE_SECRET_KEY' }, // admin sets STRIPE_SECRET_KEY }); // crm_account gains `stripe_available` (currency) and `stripe_synced_at` (datetime). From e26acddd2af42a9c06c607712735a556996ab8ee Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:38:12 +0000 Subject: [PATCH 3/3] docs(skills,automation): connectorConfig.input is judged at publish since #27968 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the #27968 fix on main, registerFlow and os validate refuse a {…} token (and a malformed or template envelope) in connectorConfig.input at connectorConfig.input., so the recipe's "{…}, {{ }} are literal text" is no longer true. The connector_action row and recipe Holds bullet 2 now teach the publish-time refusal, that a value from the record is a CEL envelope, and the run-time inputSchema check that os validate does not perform yet. Recipe 4 gets its runAs reason back. Eval 1's rubric names the inputSchema check and drops a clause the recipe no longer teaches; its must_contain tokens are unchanged. Paid by same-file restatement cuts: SKILL.md drops the record_change row's binding aside (the Trigger Types heading names the start node's config.triggerType), the node-types lead's "e.g. approval" (the requires table names the plugin), "bare CEL;" in the trigger example (rule 4 and the CEL blockquote) and "the surface just never runs" (the requires table's Absent column); the recipe drops the answer bullet's CEL example (recipe 1 reads the same path), recipe 1's "admin sets" comment (the intro says the administrator supplies the ref), the "ship it in the package" clause (the intro says a spec ships in the package) and the signing bullet's "same handling" (the section heading). Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cwp1Wc6nB6ePmS4ePn --- skills/objectstack-automation/SKILL.md | 15 +++++------ .../evals/integrations-declared.json | 2 +- .../references/integration-recipes.md | 27 +++++++++---------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 173dba935a6..c77afb774ee 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -46,13 +46,12 @@ A **Flow** is a directed graph of nodes — the primary automation building bloc | `autolaunched` | Runs without user interaction — triggered by events, APIs, or other flows | | `screen` | Interactive — presents UI screens to the user (wizards, forms) | | `schedule` | Runs on a cron/interval cadence declared on the **start node's `config.schedule`** — or a **per-record date sweep** via `config.timeRelative`, see *Time-relative triggers* | -| `record_change` | Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`). `autolaunched` + the same `record-*` binding behaves identically; `record_change` also opts into the trigger-readiness lint | +| `record_change` | Fires automatically on record create/update/delete. `autolaunched` + the same `record-*` binding behaves identically; `record_change` also opts into the trigger-readiness lint | | `api` | Inbound **webhook** — needs a start-node `secretRef` (or a literal `secret`; see *Inbound webhook triggers* below); a flow only ever started explicitly is `autolaunched` | ### Flow Node Types -Flows are built from **built-in node types** (plugins register more, e.g. -`approval`): +Flows are built from **built-in node types** (plugins register more): #### Control Flow @@ -85,7 +84,7 @@ Flows are built from **built-in node types** (plugins register more, e.g. |:-----|:--------| | `http` | Call an external HTTP API — always set `timeoutMs` (unset = no timeout). `url` and `headers` are served as written with the definition to every member who can read flows — only `signingSecret` and a start node's `secret` are withheld — so never a credential there: a credential means a declared connector called via `connector_action` | | `notify` | Send a notification through the messaging service (inbox by default) | -| `connector_action` | Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow` `input`, judged once evaluated against the action's `inputSchema` (`required`, `type`, `properties`, `items`) before dispatch: a breach fails the step `VALIDATION_ERROR` (`fault`-routable), nothing sent. `os validate` does not judge the `inputSchema` fit yet | +| `connector_action` | Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow` `input`: `registerFlow` and `os validate` refuse a `{…}` token at `connectorConfig.input.`. Once evaluated, it is judged against the action's `inputSchema` (`required`, `type`, `properties`, `items`) before dispatch: a breach fails the step `VALIDATION_ERROR` (`fault`-routable), nothing sent; `os validate` does not judge the `inputSchema` fit yet | | `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) | | `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen. `'record_link'` (never `'record'`, an object field type) takes no input — it shows the `reference` record whose id `defaultValue` evaluates to, by display name, as a link to its record page: the result screen of a flow that created records, `{ name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }` over a `create_record` `outputVariable: 'account'`. Both keys required; `required` / `options` / `placeholder` / `min` / `max` refused; a resume value under its name refused (`400 INVALID_SCREEN_INPUT`). A completed `end` `message` is refused and a markdown link in a screen `description` renders literally. Next / Submit and "Step N of M" are not authored: the engine reads the graph when the screen pauses and serves `progress` (`nextScreen: 'none' | 'possible' | 'certain'`; `step` / `stepCount` only when every path agrees), which alone labels the button and draws the counter. A `decision`, `fault` edge, loop back, non-screen pause (`approval`, `wait`) or `subflow` after a screen answers `possible` — "Next", no count. `progress` in a node `config` is refused; never hand-roll a button label or step counter into `fields` | @@ -328,9 +327,9 @@ node's `config`. ### Prerequisite — declare the capabilities your nodes need -Metadata registers without its `defineStack({ requires })` token; the surface -just never runs. Two pairs are instead **refused** at `os validate` / `build` / -`dev` / `serve` (422); neither token implies the other: +Metadata registers without its `defineStack({ requires })` token. Two pairs are +instead **refused** at `os validate` / `build` / `dev` / `serve` (422); neither +token implies the other: | `requires` token | Turns on | Absent ⇒ | |:--|:--|:--| @@ -383,7 +382,7 @@ owns `label`, `type`, `nodes` and the `edges` `FlowSchema` requires): config: { objectName: 'support_case', triggerType: 'record-after-update', - // bare CEL; gates whether the flow launches on the event + // gates whether the flow launches on the event condition: "previous.status != 'escalated' && record.status == 'escalated'", }, } diff --git a/skills/objectstack-automation/evals/integrations-declared.json b/skills/objectstack-automation/evals/integrations-declared.json index a575bd17b2f..b9827409a40 100644 --- a/skills/objectstack-automation/evals/integrations-declared.json +++ b/skills/objectstack-automation/evals/integrations-declared.json @@ -4,7 +4,7 @@ { "id": 1, "prompt": "Our contract-lifecycle app (objects `contract`, `crm_account`) needs DocuSign: send a contract out for signature, read an envelope's status, and reflect status changes on the record. DocuSign publishes a 4.7 MB Swagger 2.0 document with several hundred operations, authenticates with OAuth 2.0, and its Connect webhooks sign posts with their own HMAC header. Propose what to build and where.", - "expected_output": "A declarative instance inside the app, not a platform plugin and not a `connector-esign` family: `defineConnector` with `provider: 'openapi'`, whose `providerConfig.spec` is a packaged OpenAPI 3.x document converted offline from the Swagger 2.0 document and trimmed to the three operations (create envelope, get envelope, list status changes) (or `providerConfig.include` names those three operation keys, the `mcp` provider's allowlist meaning, where a non-array or unknown entry is fatal; trimming still keeps the package small). Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Two gaps are named plainly, each blocked on its platform card: the declarative `auth` union has no `oauth2`, so the integration is marked **blocked** on outbound OAuth (no short-lived token parked behind a `bearer` `credentialRef`); DocuSign Connect signs its own header, which the `api` door does not verify, so status is polled until vendor schemes are verified. `connectorConfig.input` is a value slot: the per-contract payload reads the record through a CEL envelope (`envelopeId: { dialect: 'cel', source: 'record.envelope_id' }`). The answer proposes no `registerConnector` code and no plugin.", + "expected_output": "A declarative instance inside the app, not a platform plugin and not a `connector-esign` family: `defineConnector` with `provider: 'openapi'`, whose `providerConfig.spec` is a packaged OpenAPI 3.x document converted offline from the Swagger 2.0 document and trimmed to the three operations (create envelope, get envelope, list status changes) (or `providerConfig.include` names those three operation keys, the `mcp` provider's allowlist meaning, where a non-array or unknown entry is fatal). Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Two gaps are named plainly, each blocked on its platform card: the declarative `auth` union has no `oauth2`, so the integration is marked **blocked** on outbound OAuth (no short-lived token parked behind a `bearer` `credentialRef`); DocuSign Connect signs its own header, which the `api` door does not verify, so status is polled until vendor schemes are verified. `connectorConfig.input` is a value slot: the per-contract payload reads the record through a CEL envelope (`envelopeId: { dialect: 'cel', source: 'record.envelope_id' }`), judged against the action's `inputSchema` before dispatch. The answer proposes no `registerConnector` code and no plugin.", "files": [], "assertions": { "must_contain": ["defineConnector", "provider: 'openapi'", "credentialRef", "connector_action", "type: 'api'", "secretRef", "Swagger 2.0", "blocked"], diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index 6ef3f927113..68385555d19 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -16,17 +16,17 @@ deployment and to the sender. Scaffolded apps list the three executors in - Actions come from the upstream (an OpenAPI `operationId`, an MCP tool name, the `rest` provider's single `request`) — authored on the entry, they are refused. - `connectorConfig.input` is a value-slot map like `subflow` `input`: a top-level - CEL envelope is evaluated, any other value kept as written (`{…}`, `{{ }}` are - literal text). Before dispatch the result is judged against the action's - `inputSchema`: `required`, `type` (with OpenAPI `nullable`), `properties`, - `items`; `null` on an optional parameter is absent. A breach fails the step - `VALIDATION_ERROR` (`fault`-routable) naming each path, nothing sent. Text - is never converted: cast a string-held id, + CEL envelope is evaluated, any other value kept as written. `registerFlow` and + `os validate` refuse a `{…}` token at `connectorConfig.input.`: a value + from the record is a CEL envelope. Before dispatch the result is judged + against the action's `inputSchema`: `required`, `type` (with OpenAPI + `nullable`), `properties`, `items`; `null` on an optional parameter is + absent. A breach fails the step `VALIDATION_ERROR` (`fault`-routable) naming + each path, nothing sent. Text is never converted: cast a string-held id, `path: { dialect: 'cel', source: '{"petId": int(record.vendor_id)}' }`. `os validate` does not judge the `inputSchema` fit yet. - The answer lands as `.` (`rest` / `openapi`: `{ status, ok, body }`; - `mcp`: `{ ok, content, structuredContent? }`), read from a later node's value - slot: `{ dialect: 'cel', source: 'bal.body.available[0].amount' }`. + `mcp`: `{ ok, content, structuredContent? }`), read from a later node's value slot. - `auth`, each variant with a `credentialRef`: `bearer`; `api-key` (`headerName`, default `X-API-Key`, or `paramName` for a query-string key); `basic` (`username` stays in metadata). A secret in a url path has no variant — use a @@ -36,12 +36,12 @@ deployment and to the sender. Scaffolded apps list the three executors in ## 1 · OpenAPI document, static key — Stripe balance, nightly ```ts -// Trim the vendor's OpenAPI 3.x document to the operations the app calls and ship -// it in the package, package-relative. +// Trim the vendor's OpenAPI 3.x document to the operations the app calls; its path +// is package-relative. export const StripeConnector = defineConnector({ name: 'stripe', label: 'Stripe', type: 'api', provider: 'openapi', providerConfig: { spec: './src/connectors/stripe.trimmed.json' }, - auth: { type: 'bearer', credentialRef: 'STRIPE_SECRET_KEY' }, // admin sets STRIPE_SECRET_KEY + auth: { type: 'bearer', credentialRef: 'STRIPE_SECRET_KEY' }, }); // crm_account gains `stripe_available` (currency) and `stripe_synced_at` (datetime). export const StripeBalanceSweep = defineFlow({ @@ -108,7 +108,7 @@ export const WarehouseApi = defineConnector({ ```ts export const CarrierCallback = defineFlow({ name: 'carrier_callback', label: 'Carrier status callback', type: 'api', - runAs: 'system', + runAs: 'system', // no trigger user nodes: [ { id: 'start', type: 'start', label: 'POST …/hooks/carrier_callback/carrier', config: { hookId: 'carrier', secretRef: 'CARRIER_HOOK_SECRET' } }, @@ -153,5 +153,4 @@ Each `operationId` is a named action in the flow palette and to agents; lifecycle is not delivered in the open tier. Do not park a short-lived access token behind a `bearer` ref. Blocked on the platform's outbound-OAuth card. - **Signature-authenticated vendor** (Alipay, WeChat Pay, Aliyun — RSA2 / HMAC - canonical-request signing): no auth kind signs requests today; same handling, - blocked on the request-signing card. + canonical-request signing): no auth kind signs requests today; blocked on the request-signing card.