Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/27982-automation-connector-input-slot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/skills': patch
---

docs(skills): the automation skill teaches `connector_action`'s `connectorConfig.input` as an evaluated value slot, and the `openapi` provider's `providerConfig.include`

Clause-②: no
Metadata-protocol: none

- **`connectorConfig.input` is a value-slot map**, like `subflow` `input`: a top-level CEL envelope is evaluated in the run's scope, so a per-record payload (`{ dialect: 'cel', source: 'record.stripe_id' }`) is a declarative dispatch now. Any other value is sent as written; a `{…}` or `{{ }}` in it is literal text. The skill says plainly that `os validate` does not judge this slot yet, so a malformed envelope fails the step at run time.
- **`providerConfig.include` exists on the `openapi` provider**, with the `mcp` provider's allowlist meaning: absent exposes every operation, and a non-array or an unknown entry is a fatal configuration fault. Trimming the document is still taught, as the way to keep the package small, not as the only allowlist.
- The `integrations-declared` eval's first case follows: it no longer grades either stale limit.
20 changes: 19 additions & 1 deletion scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,25 @@ export const CEILINGS = new Map([
// request body or response, so the derived schema silently loses it):
// 8705 -> 8814 bytes, 2177 -> 2204.
// ceil($(wc -c < skills/objectstack-automation/references/integration-recipes.md) / 4) == ceil(8814 / 4)
['skills/objectstack-automation/references/integration-recipes.md', 2204],
// 8814 -> 8942 (card #27982): the "Holds on every recipe" input bullet
// re-teaches `connectorConfig.input` as an evaluated value-slot map, with a
// record-field example and the not-yet-judged-at-`os validate` note, and
// recipe 1's closing sentence teaches `providerConfig.include` (the `mcp`
// allowlist meaning, a non-array or unknown entry fatal) instead of denying
// the key. Paid down first, three restatements each with a surviving home:
// recipe 1's comment "3.x only, … every `$ref` dereferenced offline" (the
// same comment says "OpenAPI 3.x document"; the paragraph under the recipe
// says to dereference every `$ref` offline), recipe 5's comment ", written
// from the vendor's docs" (its heading says write the document by hand),
// and the OAuth bullet's "four variants and" (the Holds `auth` bullet lists
// them). Arithmetic: 8814 -> 8942 (+128 net) -> ceil(8942 / 4) = 2236;
// headroom was 0 tokens, so the ceiling moves +32, pinned AT the
// measurement. Maintainer ruling 2026-10-27 on the seat's decision card
// 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim
// and untranslated: 「A:先削重复,余额按实测提高」 — the standing path of
// ruling record 6513306068 on #27847.
// ceil($(wc -c < skills/objectstack-automation/references/integration-recipes.md) / 4) == ceil(8942 / 4)
['skills/objectstack-automation/references/integration-recipes.md', 2236],
// 5569 -> 5587 (card #26298, audit #26184 row A2): the approval config table
// gains `excludeSubmitter` (refused beside `onEmptyApprovers: 'auto_approve'`),
// +105 tokens. Paid down first: Best Practices 1 and 4 restated the Branching
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-automation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (a `connectors:` entry with a `provider`, § *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is sent as written — no CEL or `{{ }}` in it |
| `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 |
| `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` |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 trimmed, packaged OpenAPI 3.x document carrying only the three operations (create envelope, get envelope, list status changes), converted offline from the Swagger 2.0 document — the `openapi` provider has no `include` key, so the trimmed document is the allowlist. Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Three 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 sent as written, so a per-contract payload waits on the platform too. 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; 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.",
"files": [],
"assertions": {
"must_contain": ["defineConnector", "provider: 'openapi'", "credentialRef", "connector_action", "type: 'api'", "secretRef", "Swagger 2.0", "blocked"],
Expand Down
17 changes: 10 additions & 7 deletions skills/objectstack-automation/references/integration-recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ 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 sent **as written** — no CEL envelope, no `{{ }}`
hole — so a per-record payload (an id from `record.*`) is not a declarative
dispatch today: a gap to name, not a plugin to write.
- `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.
- The answer lands as `<nodeId>.<key>` (`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' }`.
Expand All @@ -31,7 +32,7 @@ deployment and to the sender. Scaffolded apps list the three executors in

```ts
// Trim the vendor's OpenAPI 3.x document to the operations the app calls and ship
// it in the package: 3.x only, package-relative, every `$ref` dereferenced offline.
// 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
Expand Down Expand Up @@ -60,7 +61,9 @@ export const StripeBalanceSweep = defineFlow({
`requires: ['automation', 'triggers', 'job']`. Convert Swagger 2.0 or YAML and
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. The `openapi` provider has no `include` key — the trimmed document is the allowlist.
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.

## 2 · Remote MCP server, static header — GitHub

Expand Down Expand Up @@ -124,7 +127,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, written from the vendor's docs
providerConfig: { spec: { // inline OpenAPI 3.x
openapi: '3.0.3', info: { title: 'Legacy ERP', version: '1' },
servers: [{ url: 'https://erp.example.com' }],
paths: { '/v2/open-orders': { get: { operationId: 'listOpenOrders',
Expand All @@ -141,7 +144,7 @@ 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 four variants and no `oauth2`; the authorization-code / refresh
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.
Expand Down
Loading