@withone/cli 1.39.1 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{chunk-MNJJE4PJ.js → chunk-A5AFLKCQ.js} +98 -36
- package/dist/{flow-runner-OVOGDXNR.js → flow-runner-26LNPU4F.js} +1 -1
- package/dist/index.js +123 -50
- package/package.json +1 -1
- package/profiles/attio/attioCompanies.json +1 -1
- package/profiles/attio/attioPeople.json +1 -1
- package/profiles/fathom/meetings.json +1 -1
- package/profiles/gmail/gmailThreads.json +1 -1
- package/profiles/google-calendar/events.json +1 -1
- package/profiles/hacker-news/topStories.json +1 -1
- package/profiles/notion/search.json +1 -1
- package/profiles/stripe/balanceTransactions.json +1 -1
- package/profiles/stripe/customers.json +1 -1
- package/skills/one/SKILL.md +2 -0
- package/skills/one/references/flows.md +8 -1
package/README.md
CHANGED
|
@@ -312,9 +312,9 @@ one sync install && one sync doctor
|
|
|
312
312
|
```
|
|
313
313
|
|
|
314
314
|
```bash
|
|
315
|
-
# Discover → init (one command: infer +
|
|
315
|
+
# Discover → init (one command: infer + late-bound connection + auto-test) → run
|
|
316
316
|
one sync models stripe
|
|
317
|
-
one sync init stripe balanceTransactions #
|
|
317
|
+
one sync init stripe balanceTransactions # connection: { platform } baked in, test auto-run
|
|
318
318
|
one sync run stripe --since 90d
|
|
319
319
|
|
|
320
320
|
# Query, search, SQL
|
|
@@ -332,6 +332,8 @@ one sync run stripe --full-refresh
|
|
|
332
332
|
|
|
333
333
|
> **Sync uses passthrough actions only.** Profiles referencing a custom/composer action are rejected at runtime. `sync models` already filters to passthrough-only; if a model has no passthrough list endpoint, compose a flow instead of syncing.
|
|
334
334
|
|
|
335
|
+
> **Connections are late-bound.** Profiles use `"connection": { "platform": "<name>", "tag"?: "..." }` instead of literal `connectionKey` strings. The key is resolved at sync time, so `one add <platform>` (re-auth) doesn't break the profile. `tag` only needed for multi-account platforms (e.g. two Gmail accounts).
|
|
336
|
+
|
|
335
337
|
| Subcommand | What it does |
|
|
336
338
|
|------------|-------------|
|
|
337
339
|
| `install` / `doctor` | Install + verify the SQLite engine |
|
|
@@ -98,6 +98,42 @@ var OneApi = class {
|
|
|
98
98
|
async deleteConnection(id) {
|
|
99
99
|
await this.requestFull({ path: `/vault/connections/${id}`, method: "DELETE" });
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a late-bound `ConnectionRef` to a current `Connection`. Pass
|
|
103
|
+
* `cache` (a pre-fetched connection list) when resolving many refs in a
|
|
104
|
+
* loop, so each resolve doesn't repeat the listConnections round-trip.
|
|
105
|
+
*
|
|
106
|
+
* Errors are deliberately verbose: a sync profile or flow that fails to
|
|
107
|
+
* resolve a connection should surface *why* (no match / wrong tag /
|
|
108
|
+
* ambiguous) so the agent can fix the ref without trial and error.
|
|
109
|
+
*/
|
|
110
|
+
async resolveConnection(ref, cache) {
|
|
111
|
+
const all = cache ?? await this.listConnections();
|
|
112
|
+
const platformLower = ref.platform.toLowerCase();
|
|
113
|
+
const candidates = all.filter((c) => c.platform.toLowerCase() === platformLower);
|
|
114
|
+
if (candidates.length === 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`No connection found for platform "${ref.platform}". Run 'one add ${ref.platform}' to connect.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
let matches = candidates;
|
|
120
|
+
if (ref.tag) {
|
|
121
|
+
matches = candidates.filter((c) => c.tags?.includes(ref.tag));
|
|
122
|
+
if (matches.length === 0) {
|
|
123
|
+
const availableTags = candidates.flatMap((c) => c.tags ?? []);
|
|
124
|
+
throw new Error(
|
|
125
|
+
`No "${ref.platform}" connection has tag "${ref.tag}". Available tags: ${availableTags.length > 0 ? availableTags.join(", ") : "(none)"}.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (matches.length > 1) {
|
|
130
|
+
const tagList = matches.map((c) => c.tags?.length ? c.tags.join(",") : "(no tag)").join("; ");
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Multiple "${ref.platform}" connections found (tags: ${tagList}). Add a "tag" field to the connection ref to disambiguate.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return matches[0];
|
|
136
|
+
}
|
|
101
137
|
async listPlatforms() {
|
|
102
138
|
const allPlatforms = [];
|
|
103
139
|
let page = 1;
|
|
@@ -775,7 +811,29 @@ async function executeActionStep(step, context, api, permissions, allowedActionI
|
|
|
775
811
|
const action = step.action;
|
|
776
812
|
const platform = resolveValue(action.platform, context);
|
|
777
813
|
const actionId = resolveValue(action.actionId, context);
|
|
778
|
-
const
|
|
814
|
+
const hasKey = action.connectionKey !== void 0 && action.connectionKey !== null && action.connectionKey !== "";
|
|
815
|
+
const hasRef = !!action.connection?.platform;
|
|
816
|
+
if (hasKey && hasRef) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`Action step "${step.id}" has both "connectionKey" and "connection" \u2014 set exactly one. Prefer "connection: { platform, tag? }" so re-auth doesn't break the flow.`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
if (!hasKey && !hasRef) {
|
|
822
|
+
throw new Error(
|
|
823
|
+
`Action step "${step.id}" must set "connection: { platform: <name> }" (or legacy "connectionKey: <key>").`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
let connectionKey;
|
|
827
|
+
if (hasKey) {
|
|
828
|
+
connectionKey = resolveValue(action.connectionKey, context);
|
|
829
|
+
} else {
|
|
830
|
+
const ref = resolveValue(action.connection, context);
|
|
831
|
+
if (!context._connections) {
|
|
832
|
+
context._connections = await api.listConnections();
|
|
833
|
+
}
|
|
834
|
+
const conn = await api.resolveConnection(ref, context._connections);
|
|
835
|
+
connectionKey = conn.key;
|
|
836
|
+
}
|
|
779
837
|
const data = action.data ? resolveValue(action.data, context) : void 0;
|
|
780
838
|
const pathVars = action.pathVars ? resolveValue(action.pathVars, context) : void 0;
|
|
781
839
|
const queryParams = action.queryParams ? resolveValue(action.queryParams, context) : void 0;
|
|
@@ -1079,7 +1137,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
1079
1137
|
if (flowStack.includes(resolvedKey)) {
|
|
1080
1138
|
throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
|
|
1081
1139
|
}
|
|
1082
|
-
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-
|
|
1140
|
+
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-26LNPU4F.js");
|
|
1083
1141
|
const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
|
|
1084
1142
|
const subContext = await executeFlow(
|
|
1085
1143
|
subFlow,
|
|
@@ -1625,7 +1683,8 @@ var FLOW_SCHEMA = {
|
|
|
1625
1683
|
fields: {
|
|
1626
1684
|
platform: { type: "string", required: true, description: "Platform name (kebab-case)" },
|
|
1627
1685
|
actionId: { type: "string", required: true, description: "Action ID from `actions search`" },
|
|
1628
|
-
|
|
1686
|
+
connection: { type: "object", required: false, description: "Late-bound connection ref { platform, tag? } \u2014 survives re-auth. Exactly one of `connection` or `connectionKey` must be set." },
|
|
1687
|
+
connectionKey: { type: "string", required: false, description: "Literal connection key (or $.input selector). Legacy form \u2014 prefer `connection: { platform, tag? }`. Exactly one of `connection` or `connectionKey` must be set." },
|
|
1629
1688
|
data: { type: "object", required: false, description: "Request body (POST/PUT/PATCH)" },
|
|
1630
1689
|
pathVars: { type: "object", required: false, description: "URL path variables" },
|
|
1631
1690
|
queryParams: { type: "object", required: false, description: "Query parameters" },
|
|
@@ -1638,7 +1697,7 @@ var FLOW_SCHEMA = {
|
|
|
1638
1697
|
action: {
|
|
1639
1698
|
platform: "stripe",
|
|
1640
1699
|
actionId: "conn_mod_def::xxx::yyy",
|
|
1641
|
-
|
|
1700
|
+
connection: { platform: "stripe" },
|
|
1642
1701
|
data: { query: "email:'{{$.input.customerEmail}}'" }
|
|
1643
1702
|
}
|
|
1644
1703
|
}
|
|
@@ -1687,7 +1746,7 @@ var FLOW_SCHEMA = {
|
|
|
1687
1746
|
type: "condition",
|
|
1688
1747
|
condition: {
|
|
1689
1748
|
expression: "$.steps.search.response.data.length > 0",
|
|
1690
|
-
then: [{ id: "notify", name: "Send notification", type: "action", action: { platform: "slack", actionId: "...",
|
|
1749
|
+
then: [{ id: "notify", name: "Send notification", type: "action", action: { platform: "slack", actionId: "...", connection: { platform: "slack" }, data: { text: "Found!" } } }],
|
|
1691
1750
|
else: [{ id: "logMiss", name: "Log not found", type: "transform", transform: { expression: "'Not found'" } }]
|
|
1692
1751
|
}
|
|
1693
1752
|
}
|
|
@@ -1711,7 +1770,7 @@ var FLOW_SCHEMA = {
|
|
|
1711
1770
|
loop: {
|
|
1712
1771
|
over: "$.steps.listOrders.response.data",
|
|
1713
1772
|
as: "order",
|
|
1714
|
-
steps: [{ id: "createInvoice", name: "Create invoice", type: "action", action: { platform: "stripe", actionId: "...",
|
|
1773
|
+
steps: [{ id: "createInvoice", name: "Create invoice", type: "action", action: { platform: "stripe", actionId: "...", connection: { platform: "stripe" }, data: { amount: "$.loop.order.total" } } }]
|
|
1715
1774
|
}
|
|
1716
1775
|
}
|
|
1717
1776
|
},
|
|
@@ -1729,8 +1788,8 @@ var FLOW_SCHEMA = {
|
|
|
1729
1788
|
type: "parallel",
|
|
1730
1789
|
parallel: {
|
|
1731
1790
|
steps: [
|
|
1732
|
-
{ id: "getStripe", name: "Get Stripe data", type: "action", action: { platform: "stripe", actionId: "...",
|
|
1733
|
-
{ id: "getSlack", name: "Get Slack data", type: "action", action: { platform: "slack", actionId: "...",
|
|
1791
|
+
{ id: "getStripe", name: "Get Stripe data", type: "action", action: { platform: "stripe", actionId: "...", connection: { platform: "stripe" } } },
|
|
1792
|
+
{ id: "getSlack", name: "Get Slack data", type: "action", action: { platform: "slack", actionId: "...", connection: { platform: "slack" } } }
|
|
1734
1793
|
]
|
|
1735
1794
|
}
|
|
1736
1795
|
}
|
|
@@ -1782,7 +1841,7 @@ var FLOW_SCHEMA = {
|
|
|
1782
1841
|
while: {
|
|
1783
1842
|
condition: "$.steps.paginate.output.lastResult.nextPageToken != null",
|
|
1784
1843
|
maxIterations: 50,
|
|
1785
|
-
steps: [{ id: "fetchPage", name: "Fetch next page", type: "action", action: { platform: "gmail", actionId: "...",
|
|
1844
|
+
steps: [{ id: "fetchPage", name: "Fetch next page", type: "action", action: { platform: "gmail", actionId: "...", connection: { platform: "gmail" } } }]
|
|
1786
1845
|
}
|
|
1787
1846
|
}
|
|
1788
1847
|
},
|
|
@@ -1806,7 +1865,7 @@ var FLOW_SCHEMA = {
|
|
|
1806
1865
|
configKey: "paginate",
|
|
1807
1866
|
description: "Auto-paginate API results into a single array",
|
|
1808
1867
|
fields: {
|
|
1809
|
-
action: { type: "object", required: true, description: "Action config (same shape as action step: platform, actionId, connectionKey)" },
|
|
1868
|
+
action: { type: "object", required: true, description: "Action config (same shape as action step: platform, actionId, plus exactly one of `connection` or `connectionKey`)" },
|
|
1810
1869
|
pageTokenField: { type: "string", required: true, description: "Dot-path in response to next page token" },
|
|
1811
1870
|
resultsField: { type: "string", required: true, description: "Dot-path in response to results array" },
|
|
1812
1871
|
inputTokenParam: { type: "string", required: true, description: "Dot-path in action config where page token is injected" },
|
|
@@ -1817,7 +1876,7 @@ var FLOW_SCHEMA = {
|
|
|
1817
1876
|
name: "Fetch all Gmail messages",
|
|
1818
1877
|
type: "paginate",
|
|
1819
1878
|
paginate: {
|
|
1820
|
-
action: { platform: "gmail", actionId: "...",
|
|
1879
|
+
action: { platform: "gmail", actionId: "...", connection: { platform: "gmail" }, queryParams: { maxResults: 100 } },
|
|
1821
1880
|
pageTokenField: "nextPageToken",
|
|
1822
1881
|
resultsField: "messages",
|
|
1823
1882
|
inputTokenParam: "queryParams.pageToken",
|
|
@@ -2015,12 +2074,6 @@ The only differences: (1) prepend the stdin-read line, (2) replace \`return X\`
|
|
|
2015
2074
|
"description": "What this flow does",
|
|
2016
2075
|
"version": "1",
|
|
2017
2076
|
"inputs": {
|
|
2018
|
-
"connectionKey": {
|
|
2019
|
-
"type": "string",
|
|
2020
|
-
"required": true,
|
|
2021
|
-
"description": "Platform connection key",
|
|
2022
|
-
"connection": { "platform": "stripe" }
|
|
2023
|
-
},
|
|
2024
2077
|
"param": {
|
|
2025
2078
|
"type": "string",
|
|
2026
2079
|
"required": true,
|
|
@@ -2035,7 +2088,7 @@ The only differences: (1) prepend the stdin-read line, (2) replace \`return X\`
|
|
|
2035
2088
|
"action": {
|
|
2036
2089
|
"platform": "stripe",
|
|
2037
2090
|
"actionId": "conn_mod_def::xxx::yyy",
|
|
2038
|
-
"
|
|
2091
|
+
"connection": { "platform": "stripe" },
|
|
2039
2092
|
"data": { "query": "{{$.input.param}}" }
|
|
2040
2093
|
}
|
|
2041
2094
|
}
|
|
@@ -2129,13 +2182,13 @@ Pipes can be applied to any value (objects/arrays are JSON-stringified first for
|
|
|
2129
2182
|
|
|
2130
2183
|
### When to use bare selectors vs \`{{...}}\` interpolation
|
|
2131
2184
|
|
|
2132
|
-
- **Bare selectors** (\`$.input.x\`): Use for fields the engine resolves directly \u2014 \`connectionKey\`, \`over\`, \`path\`, \`expression\`, \`condition\`, and any field where the entire value is a single selector. The resolved value keeps its original type (object, array, number).
|
|
2185
|
+
- **Bare selectors** (\`$.input.x\`): Use for fields the engine resolves directly \u2014 \`connectionKey\`, the \`tag\` (or \`platform\`) inside \`connection\`, \`over\`, \`path\`, \`expression\`, \`condition\`, and any field where the entire value is a single selector. The resolved value keeps its original type (object, array, number).
|
|
2133
2186
|
- **Interpolation** (\`{{$.input.x}}\`): Use inside string values where the selector is embedded in text \u2014 e.g., \`"Hello {{$.steps.getUser.response.name}}"\`. The resolved value is always stringified. Use this in \`data\`, \`pathVars\`, and \`queryParams\` when mixing selectors with literal text.
|
|
2134
2187
|
- **Rule of thumb**: If the value is purely a selector, use bare. If it's a string containing a selector, use \`{{...}}\`.
|
|
2135
2188
|
|
|
2136
2189
|
### Selectors vs expressions
|
|
2137
2190
|
|
|
2138
|
-
Selectors in data fields (\`data\`, \`queryParams\`, \`pathVars\`, \`connectionKey\`) are **dot-path lookups only** \u2014 they do not support JavaScript operators like \`||\` or \`&&\`. For default values, use the \`default\` field on the input definition:
|
|
2191
|
+
Selectors in data fields (\`data\`, \`queryParams\`, \`pathVars\`, \`connectionKey\`, \`connection.tag\`) are **dot-path lookups only** \u2014 they do not support JavaScript operators like \`||\` or \`&&\`. For default values, use the \`default\` field on the input definition:
|
|
2139
2192
|
|
|
2140
2193
|
\`\`\`json
|
|
2141
2194
|
{ "inputs": { "maxResults": { "type": "number", "default": 10 } } }
|
|
@@ -2255,9 +2308,30 @@ A \`flow\` step's \`flow.key\` accepts selectors and Handlebars interpolations,
|
|
|
2255
2308
|
|
|
2256
2309
|
Conditional execution: \`"if": "$.steps.prev.response.data.length > 0"\`
|
|
2257
2310
|
|
|
2258
|
-
##
|
|
2311
|
+
## Connection Resolution \u2014 late-bound by default
|
|
2312
|
+
|
|
2313
|
+
Action steps reference a platform connection in one of two forms:
|
|
2314
|
+
|
|
2315
|
+
\`\`\`json
|
|
2316
|
+
// preferred \u2014 late-bound, survives re-auth
|
|
2317
|
+
"connection": { "platform": "gmail" }
|
|
2318
|
+
|
|
2319
|
+
// multi-account: disambiguate with the connection's tag
|
|
2320
|
+
"connection": { "platform": "gmail", "tag": "work@example.com" }
|
|
2321
|
+
|
|
2322
|
+
// legacy \u2014 works for backwards compat, breaks on re-auth
|
|
2323
|
+
"connectionKey": "live::gmail::default::abc123..."
|
|
2324
|
+
\`\`\`
|
|
2325
|
+
|
|
2326
|
+
The engine resolves the \`connection\` ref once per flow run (cached for the run's lifetime) by calling \`listConnections\` and matching on platform + optional tag. Resolution errors fail the step with a clear message \u2014 \`No connection found for platform "X"\`, \`Multiple "X" connections found (tags: ...). Add a "tag" field\`, or \`No "X" connection has tag "Y"\`.
|
|
2259
2327
|
|
|
2260
|
-
|
|
2328
|
+
Both \`platform\` and \`tag\` accept \`$.input.x\` selectors so a flow can be parameterised per-execution (e.g. multi-tenant orchestrators that pass the user's email as the tag).
|
|
2329
|
+
|
|
2330
|
+
The validator rejects any action that sets both forms or neither, at \`flow validate\` and \`flow execute\` time.
|
|
2331
|
+
|
|
2332
|
+
### Optional input metadata: connection-key auto-resolve (legacy)
|
|
2333
|
+
|
|
2334
|
+
For flows that still use literal \`connectionKey\` strings via inputs, an input declaration can carry a \`"connection": { "platform": "..." }\` hint so \`flow execute\` auto-fills a single matching connection's key. New flows don't need this \u2014 switch the action's connection form to \`{ platform, tag? }\` and skip the input entirely.
|
|
2261
2335
|
|
|
2262
2336
|
## Complete Example: Fetch Data, Transform, Notify
|
|
2263
2337
|
|
|
@@ -2268,18 +2342,6 @@ When an input has \`"connection": { "platform": "stripe" }\`, the flow engine ca
|
|
|
2268
2342
|
"description": "Fetch recent contacts from CRM, build a summary, post to Slack",
|
|
2269
2343
|
"version": "1",
|
|
2270
2344
|
"inputs": {
|
|
2271
|
-
"crmConnectionKey": {
|
|
2272
|
-
"type": "string",
|
|
2273
|
-
"required": true,
|
|
2274
|
-
"description": "CRM platform connection key",
|
|
2275
|
-
"connection": { "platform": "attio" }
|
|
2276
|
-
},
|
|
2277
|
-
"slackConnectionKey": {
|
|
2278
|
-
"type": "string",
|
|
2279
|
-
"required": true,
|
|
2280
|
-
"description": "Slack connection key",
|
|
2281
|
-
"connection": { "platform": "slack" }
|
|
2282
|
-
},
|
|
2283
2345
|
"slackChannel": {
|
|
2284
2346
|
"type": "string",
|
|
2285
2347
|
"required": true,
|
|
@@ -2294,7 +2356,7 @@ When an input has \`"connection": { "platform": "stripe" }\`, the flow engine ca
|
|
|
2294
2356
|
"action": {
|
|
2295
2357
|
"platform": "attio",
|
|
2296
2358
|
"actionId": "ATTIO_LIST_PEOPLE_ACTION_ID",
|
|
2297
|
-
"
|
|
2359
|
+
"connection": { "platform": "attio" },
|
|
2298
2360
|
"queryParams": { "limit": "10" }
|
|
2299
2361
|
}
|
|
2300
2362
|
},
|
|
@@ -2313,7 +2375,7 @@ When an input has \`"connection": { "platform": "stripe" }\`, the flow engine ca
|
|
|
2313
2375
|
"action": {
|
|
2314
2376
|
"platform": "slack",
|
|
2315
2377
|
"actionId": "SLACK_SEND_MESSAGE_ACTION_ID",
|
|
2316
|
-
"
|
|
2378
|
+
"connection": { "platform": "slack" },
|
|
2317
2379
|
"data": {
|
|
2318
2380
|
"channel": "$.input.slackChannel",
|
|
2319
2381
|
"text": "{{$.steps.buildSummary.output.summary}}"
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
resolveFlowPath,
|
|
20
20
|
saveFlow,
|
|
21
21
|
validateActionInput
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-A5AFLKCQ.js";
|
|
23
23
|
|
|
24
24
|
// src/index.ts
|
|
25
25
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -3269,10 +3269,13 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
3269
3269
|
const a = value;
|
|
3270
3270
|
if (!a.platform) errors.push({ path: `${fieldPath}.platform`, message: 'Action must have "platform"' });
|
|
3271
3271
|
if (!a.actionId) errors.push({ path: `${fieldPath}.actionId`, message: 'Action must have "actionId"' });
|
|
3272
|
-
|
|
3272
|
+
validateConnectionForm(a, fieldPath, errors);
|
|
3273
3273
|
}
|
|
3274
3274
|
}
|
|
3275
3275
|
}
|
|
3276
|
+
if (descriptor.type === "action") {
|
|
3277
|
+
validateConnectionForm(config2, `${path17}.${configKey}`, errors);
|
|
3278
|
+
}
|
|
3276
3279
|
if (descriptor.type === "code") {
|
|
3277
3280
|
const hasSource = typeof config2.source === "string" && config2.source.length > 0;
|
|
3278
3281
|
const hasModule = typeof config2.module === "string" && config2.module.length > 0;
|
|
@@ -3300,6 +3303,43 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
3300
3303
|
}
|
|
3301
3304
|
}
|
|
3302
3305
|
}
|
|
3306
|
+
function validateConnectionForm(config2, pathPrefix, errors) {
|
|
3307
|
+
const hasKey = config2.connectionKey !== void 0 && config2.connectionKey !== null && config2.connectionKey !== "";
|
|
3308
|
+
const conn = config2.connection;
|
|
3309
|
+
const hasRef = !!conn && typeof conn === "object" && !Array.isArray(conn) && typeof conn.platform === "string" && conn.platform.length > 0;
|
|
3310
|
+
if (hasKey && hasRef) {
|
|
3311
|
+
errors.push({
|
|
3312
|
+
path: pathPrefix,
|
|
3313
|
+
message: `Action has both "connectionKey" and "connection" \u2014 set exactly one. Prefer "connection: { platform, tag? }" so re-auth doesn't break the flow.`
|
|
3314
|
+
});
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
if (!hasKey && !hasRef) {
|
|
3318
|
+
errors.push({
|
|
3319
|
+
path: pathPrefix,
|
|
3320
|
+
message: 'Action must set "connection: { platform: <name> }" (or legacy "connectionKey: <key>").'
|
|
3321
|
+
});
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
if (hasRef) {
|
|
3325
|
+
const c = conn;
|
|
3326
|
+
if (c.tag !== void 0 && typeof c.tag !== "string") {
|
|
3327
|
+
errors.push({
|
|
3328
|
+
path: `${pathPrefix}.connection.tag`,
|
|
3329
|
+
message: '"connection.tag" must be a string when set'
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
const allowed = /* @__PURE__ */ new Set(["platform", "tag"]);
|
|
3333
|
+
for (const k of Object.keys(c)) {
|
|
3334
|
+
if (!allowed.has(k)) {
|
|
3335
|
+
errors.push({
|
|
3336
|
+
path: `${pathPrefix}.connection.${k}`,
|
|
3337
|
+
message: `Unknown field "${k}" in connection ref. Allowed: platform, tag.`
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3303
3343
|
function detectFlatConfigHint(step, descriptor) {
|
|
3304
3344
|
const requiredFields = Object.entries(descriptor.fields).filter(([, fd]) => fd.required).map(([name]) => name);
|
|
3305
3345
|
const flatFields = requiredFields.filter((f) => f in step);
|
|
@@ -4107,14 +4147,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4107
4147
|
name: "My Workflow",
|
|
4108
4148
|
description: "A basic workflow with a single action step",
|
|
4109
4149
|
version: "1",
|
|
4110
|
-
inputs: {
|
|
4111
|
-
connectionKey: {
|
|
4112
|
-
type: "string",
|
|
4113
|
-
required: true,
|
|
4114
|
-
description: "Connection key for the platform",
|
|
4115
|
-
connection: { platform: "PLATFORM_NAME" }
|
|
4116
|
-
}
|
|
4117
|
-
},
|
|
4150
|
+
inputs: {},
|
|
4118
4151
|
steps: [
|
|
4119
4152
|
{
|
|
4120
4153
|
id: "step1",
|
|
@@ -4123,7 +4156,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4123
4156
|
action: {
|
|
4124
4157
|
platform: "PLATFORM_NAME",
|
|
4125
4158
|
actionId: "ACTION_ID_FROM_SEARCH",
|
|
4126
|
-
|
|
4159
|
+
connection: { platform: "PLATFORM_NAME" },
|
|
4127
4160
|
data: {}
|
|
4128
4161
|
}
|
|
4129
4162
|
}
|
|
@@ -4134,14 +4167,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4134
4167
|
name: "Conditional Workflow",
|
|
4135
4168
|
description: "Fetch data, then branch based on results",
|
|
4136
4169
|
version: "1",
|
|
4137
|
-
inputs: {
|
|
4138
|
-
connectionKey: {
|
|
4139
|
-
type: "string",
|
|
4140
|
-
required: true,
|
|
4141
|
-
description: "Connection key",
|
|
4142
|
-
connection: { platform: "PLATFORM_NAME" }
|
|
4143
|
-
}
|
|
4144
|
-
},
|
|
4170
|
+
inputs: {},
|
|
4145
4171
|
steps: [
|
|
4146
4172
|
{
|
|
4147
4173
|
id: "fetch",
|
|
@@ -4150,7 +4176,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4150
4176
|
action: {
|
|
4151
4177
|
platform: "PLATFORM_NAME",
|
|
4152
4178
|
actionId: "ACTION_ID_FROM_SEARCH",
|
|
4153
|
-
|
|
4179
|
+
connection: { platform: "PLATFORM_NAME" }
|
|
4154
4180
|
}
|
|
4155
4181
|
},
|
|
4156
4182
|
{
|
|
@@ -4184,14 +4210,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4184
4210
|
name: "Loop Workflow",
|
|
4185
4211
|
description: "Fetch a list, then process each item",
|
|
4186
4212
|
version: "1",
|
|
4187
|
-
inputs: {
|
|
4188
|
-
connectionKey: {
|
|
4189
|
-
type: "string",
|
|
4190
|
-
required: true,
|
|
4191
|
-
description: "Connection key",
|
|
4192
|
-
connection: { platform: "PLATFORM_NAME" }
|
|
4193
|
-
}
|
|
4194
|
-
},
|
|
4213
|
+
inputs: {},
|
|
4195
4214
|
steps: [
|
|
4196
4215
|
{
|
|
4197
4216
|
id: "fetchList",
|
|
@@ -4200,7 +4219,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4200
4219
|
action: {
|
|
4201
4220
|
platform: "PLATFORM_NAME",
|
|
4202
4221
|
actionId: "ACTION_ID_FROM_SEARCH",
|
|
4203
|
-
|
|
4222
|
+
connection: { platform: "PLATFORM_NAME" }
|
|
4204
4223
|
}
|
|
4205
4224
|
},
|
|
4206
4225
|
{
|
|
@@ -4233,14 +4252,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4233
4252
|
name: "AI Analysis Workflow",
|
|
4234
4253
|
description: "Fetch data, analyze with Claude, and send results",
|
|
4235
4254
|
version: "1",
|
|
4236
|
-
inputs: {
|
|
4237
|
-
connectionKey: {
|
|
4238
|
-
type: "string",
|
|
4239
|
-
required: true,
|
|
4240
|
-
description: "Connection key for data source",
|
|
4241
|
-
connection: { platform: "PLATFORM_NAME" }
|
|
4242
|
-
}
|
|
4243
|
-
},
|
|
4255
|
+
inputs: {},
|
|
4244
4256
|
steps: [
|
|
4245
4257
|
{
|
|
4246
4258
|
id: "fetchData",
|
|
@@ -4249,7 +4261,7 @@ var SCAFFOLD_TEMPLATES = {
|
|
|
4249
4261
|
action: {
|
|
4250
4262
|
platform: "PLATFORM_NAME",
|
|
4251
4263
|
actionId: "ACTION_ID_FROM_SEARCH",
|
|
4252
|
-
|
|
4264
|
+
connection: { platform: "PLATFORM_NAME" }
|
|
4253
4265
|
}
|
|
4254
4266
|
},
|
|
4255
4267
|
{
|
|
@@ -4732,12 +4744,24 @@ function readProfile(platform, model) {
|
|
|
4732
4744
|
}
|
|
4733
4745
|
}
|
|
4734
4746
|
function writeProfile(profile) {
|
|
4735
|
-
const required = ["platform", "model", "
|
|
4747
|
+
const required = ["platform", "model", "actionId", "idField", "pagination"];
|
|
4736
4748
|
for (const field of required) {
|
|
4737
4749
|
if (!profile[field]) {
|
|
4738
4750
|
throw new Error(`Missing required field: ${field}`);
|
|
4739
4751
|
}
|
|
4740
4752
|
}
|
|
4753
|
+
const hasKey = !!profile.connectionKey;
|
|
4754
|
+
const hasRef = !!profile.connection?.platform;
|
|
4755
|
+
if (hasKey && hasRef) {
|
|
4756
|
+
throw new Error(
|
|
4757
|
+
"Profile has both `connectionKey` and `connection` \u2014 set exactly one. Prefer `connection: { platform, tag? }` so re-auth doesn't break the profile."
|
|
4758
|
+
);
|
|
4759
|
+
}
|
|
4760
|
+
if (!hasKey && !hasRef) {
|
|
4761
|
+
throw new Error(
|
|
4762
|
+
'Missing connection: set `connection: { platform: "<name>" }` (or legacy `connectionKey: "<key>"`).'
|
|
4763
|
+
);
|
|
4764
|
+
}
|
|
4741
4765
|
if (profile.resultsPath === void 0) {
|
|
4742
4766
|
throw new Error('Missing required field: resultsPath (use "" or "$" for root-array responses)');
|
|
4743
4767
|
}
|
|
@@ -4748,6 +4772,16 @@ function writeProfile(profile) {
|
|
|
4748
4772
|
const filePath = profilePath(profile.platform, profile.model);
|
|
4749
4773
|
fs8.writeFileSync(filePath, JSON.stringify(profile, null, 2));
|
|
4750
4774
|
}
|
|
4775
|
+
async function resolveProfileConnectionKey(api, profile, cache2) {
|
|
4776
|
+
if (profile.connectionKey) return profile.connectionKey;
|
|
4777
|
+
if (!profile.connection?.platform) {
|
|
4778
|
+
throw new Error(
|
|
4779
|
+
`Profile ${profile.platform}/${profile.model} has no connectionKey or connection ref.`
|
|
4780
|
+
);
|
|
4781
|
+
}
|
|
4782
|
+
const conn = await api.resolveConnection(profile.connection, cache2);
|
|
4783
|
+
return conn.key;
|
|
4784
|
+
}
|
|
4751
4785
|
function writeDraftProfile(platform, model, draft) {
|
|
4752
4786
|
fs8.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
4753
4787
|
const filePath = profilePath(platform, model);
|
|
@@ -4773,7 +4807,9 @@ function generateTemplate(platform, model, actionId) {
|
|
|
4773
4807
|
return {
|
|
4774
4808
|
platform,
|
|
4775
4809
|
model,
|
|
4776
|
-
|
|
4810
|
+
// Late-bound ref — survives re-auth. Use { platform, tag } when the
|
|
4811
|
+
// platform has multiple connections (e.g. multiple Gmail accounts).
|
|
4812
|
+
connection: { platform },
|
|
4777
4813
|
actionId: actionId ?? "FILL_IN",
|
|
4778
4814
|
resultsPath: "FILL_IN",
|
|
4779
4815
|
idField: "FILL_IN",
|
|
@@ -5833,6 +5869,7 @@ async function syncModel(api, profile, options) {
|
|
|
5833
5869
|
"--full-refresh and --since cannot be used together. --full-refresh always fetches the whole collection."
|
|
5834
5870
|
);
|
|
5835
5871
|
}
|
|
5872
|
+
const connectionKey = await resolveProfileConnectionKey(api, profile);
|
|
5836
5873
|
const lock = options.dryRun ? null : acquireSyncLock(platform, model);
|
|
5837
5874
|
const existingState = getModelState(platform, model);
|
|
5838
5875
|
if (existingState?.status === "syncing" && !options.dryRun && !isAgentMode()) {
|
|
@@ -5960,7 +5997,7 @@ async function syncModel(api, profile, options) {
|
|
|
5960
5997
|
const result = await api.executePassthroughRequest({
|
|
5961
5998
|
platform,
|
|
5962
5999
|
actionId: profile.actionId,
|
|
5963
|
-
connectionKey
|
|
6000
|
+
connectionKey,
|
|
5964
6001
|
pathVariables: profile.pathVars,
|
|
5965
6002
|
queryParams: currentPageQueryParams,
|
|
5966
6003
|
headers: currentPageHeaders,
|
|
@@ -6169,7 +6206,7 @@ async function syncModel(api, profile, options) {
|
|
|
6169
6206
|
profile.enrich,
|
|
6170
6207
|
model,
|
|
6171
6208
|
profile.idField,
|
|
6172
|
-
|
|
6209
|
+
connectionKey,
|
|
6173
6210
|
platform,
|
|
6174
6211
|
{
|
|
6175
6212
|
transform: profile.transform,
|
|
@@ -6275,6 +6312,18 @@ async function testSyncProfile(api, profile) {
|
|
|
6275
6312
|
if (limitLocation === "body") bodyParams[limitParam] = pageSize;
|
|
6276
6313
|
else queryParams[limitParam] = pageSize;
|
|
6277
6314
|
}
|
|
6315
|
+
let connectionKey;
|
|
6316
|
+
try {
|
|
6317
|
+
connectionKey = await resolveProfileConnectionKey(api, profile);
|
|
6318
|
+
checks.push({ name: "connection resolves", ok: true });
|
|
6319
|
+
} catch (err) {
|
|
6320
|
+
checks.push({
|
|
6321
|
+
name: "connection resolves",
|
|
6322
|
+
ok: false,
|
|
6323
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
6324
|
+
});
|
|
6325
|
+
return report;
|
|
6326
|
+
}
|
|
6278
6327
|
let actionDetails;
|
|
6279
6328
|
try {
|
|
6280
6329
|
actionDetails = await api.getActionDetails(profile.actionId);
|
|
@@ -6300,7 +6349,7 @@ async function testSyncProfile(api, profile) {
|
|
|
6300
6349
|
const result = await api.executePassthroughRequest({
|
|
6301
6350
|
platform: profile.platform,
|
|
6302
6351
|
actionId: profile.actionId,
|
|
6303
|
-
connectionKey
|
|
6352
|
+
connectionKey,
|
|
6304
6353
|
pathVariables: profile.pathVars,
|
|
6305
6354
|
queryParams,
|
|
6306
6355
|
data: Object.keys(bodyParams).length > 0 ? bodyParams : void 0
|
|
@@ -7459,11 +7508,11 @@ async function syncInitCommand(platform, model, options) {
|
|
|
7459
7508
|
(c) => c.platform === platform
|
|
7460
7509
|
);
|
|
7461
7510
|
if (platformConns.length === 1) {
|
|
7462
|
-
|
|
7463
|
-
inferred?.reasoning.push(`connectionKey: auto-resolved (only one ${platform} connection)`);
|
|
7511
|
+
inferred?.reasoning.push(`connection: { platform: "${platform}" } resolves to the single available connection`);
|
|
7464
7512
|
} else if (platformConns.length > 1) {
|
|
7513
|
+
const tags = platformConns.map((c) => c.tags?.join(",") ?? "(no tag)").join("; ");
|
|
7465
7514
|
inferred?.reasoning.push(
|
|
7466
|
-
`
|
|
7515
|
+
`connection: ${platformConns.length} ${platform} connections found (tags: ${tags}). Add a \`tag\` field to the connection ref to disambiguate.`
|
|
7467
7516
|
);
|
|
7468
7517
|
}
|
|
7469
7518
|
} catch {
|
|
@@ -7547,6 +7596,11 @@ Run with --config to save:
|
|
|
7547
7596
|
...patch.pagination ?? {}
|
|
7548
7597
|
}
|
|
7549
7598
|
};
|
|
7599
|
+
if (patch.connection && !patch.connectionKey) {
|
|
7600
|
+
delete profile.connectionKey;
|
|
7601
|
+
} else if (patch.connectionKey && !patch.connection) {
|
|
7602
|
+
delete profile.connection;
|
|
7603
|
+
}
|
|
7550
7604
|
try {
|
|
7551
7605
|
writeProfile(profile);
|
|
7552
7606
|
if (isAgentMode()) {
|
|
@@ -8638,12 +8692,12 @@ one --agent sync models stripe
|
|
|
8638
8692
|
# 2. Init \u2014 one command does everything:
|
|
8639
8693
|
# - resolves action ID
|
|
8640
8694
|
# - infers pagination, resultsPath, idField, pathVars from knowledge
|
|
8641
|
-
# -
|
|
8695
|
+
# - sets connection: { platform } so the profile survives re-auth
|
|
8642
8696
|
# - auto-runs sync test if profile is complete
|
|
8643
8697
|
one --agent sync init stripe balanceTransactions
|
|
8644
8698
|
# Response includes _complete:true and _test results when fully resolved.
|
|
8645
|
-
#
|
|
8646
|
-
one --agent sync init
|
|
8699
|
+
# Multi-account platforms (e.g. two Gmail connections) need a tag:
|
|
8700
|
+
one --agent sync init gmail gmailThreads --config '{"connection":{"platform":"gmail","tag":"work@example.com"}}'
|
|
8647
8701
|
|
|
8648
8702
|
# 3. Sync
|
|
8649
8703
|
one --agent sync run stripe
|
|
@@ -8654,10 +8708,29 @@ one --agent sync search "refund" --platform stripe
|
|
|
8654
8708
|
one --agent sync sql stripe "SELECT count(*) FROM balanceTransactions"
|
|
8655
8709
|
\`\`\`
|
|
8656
8710
|
|
|
8711
|
+
## Connection Resolution \u2014 late-bound by default
|
|
8712
|
+
|
|
8713
|
+
Sync profiles use a late-bound connection ref instead of a hardcoded key, so re-auth (which always mints a new key) doesn't break the profile:
|
|
8714
|
+
|
|
8715
|
+
\`\`\`json
|
|
8716
|
+
// recommended \u2014 survives re-auth
|
|
8717
|
+
"connection": { "platform": "gmail" }
|
|
8718
|
+
|
|
8719
|
+
// multi-account: disambiguate with the connection's tag
|
|
8720
|
+
"connection": { "platform": "gmail", "tag": "work@example.com" }
|
|
8721
|
+
|
|
8722
|
+
// legacy \u2014 still works for backwards compat, but breaks on re-auth
|
|
8723
|
+
"connectionKey": "live::gmail::default::abc123..."
|
|
8724
|
+
\`\`\`
|
|
8725
|
+
|
|
8726
|
+
The resolver runs at \`sync test\` and \`sync run\` time. Resolution errors (no connection, ambiguous tag, missing tag with multiple connections) surface as the first check in the test report, before any HTTP call.
|
|
8727
|
+
|
|
8728
|
+
To migrate an existing profile: replace the \`connectionKey\` field with \`connection: { platform: "<platform>" }\`. Tags only needed when more than one connection exists for the platform.
|
|
8729
|
+
|
|
8657
8730
|
## Auto-Inference
|
|
8658
8731
|
|
|
8659
8732
|
\`sync init\` without \`--config\` does all of this automatically:
|
|
8660
|
-
- **
|
|
8733
|
+
- **connection** \u2014 defaults to \`{ platform: "<platform>" }\` (late-bound). When multiple connections exist, init surfaces the available tags so the agent can add one to the ref.
|
|
8661
8734
|
- **Pagination** \u2014 Stripe id-pagination, Notion body-cursor, HubSpot/Google token, offset, link. Inapplicable fields stripped (no nextPath for offset, no passAs for none)
|
|
8662
8735
|
- **resultsPath** \u2014 generic keys (data, results, items) + platform-specific (model name stripped of platform prefix: attioCompanies \u2192 companies). Use \`""\`, \`"$"\`, or \`"."\` for responses that return a bare array at the root (e.g. Hacker News \`/v0/topstories.json\`); primitive array elements are auto-wrapped as \`{ [idField]: value }\`.
|
|
8663
8736
|
- **idField** \u2014 id, _id, uuid
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Attio companies — CRM company records with domains, industry, and custom attributes",
|
|
3
3
|
"platform": "attio",
|
|
4
4
|
"model": "attioCompanies",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "attio" },
|
|
6
6
|
"actionId": "conn_mod_def::GJt0lFZ6kpk::attio-companies-list",
|
|
7
7
|
"resultsPath": "companies",
|
|
8
8
|
"idField": "id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Attio people — CRM contact records with emails, phone numbers, and custom attributes",
|
|
3
3
|
"platform": "attio",
|
|
4
4
|
"model": "attioPeople",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "attio" },
|
|
6
6
|
"identityKey": "primary_email_address",
|
|
7
7
|
"actionId": "conn_mod_def::GJt0lFZ6kpk::attio-people-list",
|
|
8
8
|
"resultsPath": "people",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Fathom meetings — recorded meetings with transcripts, summaries, action items, and attendees",
|
|
3
3
|
"platform": "fathom",
|
|
4
4
|
"model": "meetings",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "fathom" },
|
|
6
6
|
"actionId": "conn_mod_def::fathom::meetings-list",
|
|
7
7
|
"resultsPath": "items",
|
|
8
8
|
"idField": "recording_id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Gmail email threads — primary inbox with full message bodies (no attachments)",
|
|
3
3
|
"platform": "gmail",
|
|
4
4
|
"model": "gmailThreads",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "gmail" },
|
|
6
6
|
"actionId": "conn_mod_def::GJ3ok-Q0D40::oLWNlcx4QDORaL_18z-MsQ",
|
|
7
7
|
"resultsPath": "threads",
|
|
8
8
|
"idField": "id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Google Calendar events — meetings, appointments, and all-day events with attendees and location",
|
|
3
3
|
"platform": "google-calendar",
|
|
4
4
|
"model": "events",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "google-calendar" },
|
|
6
6
|
"actionId": "conn_mod_def::GJ5x5pOh2TU::gcal-events-list",
|
|
7
7
|
"resultsPath": "items",
|
|
8
8
|
"idField": "id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Hacker News top story IDs — up to 500 item IDs at /v0/topstories.json (root-array response, primitives wrapped as { id })",
|
|
3
3
|
"platform": "hacker-news",
|
|
4
4
|
"model": "topStories",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "hacker-news" },
|
|
6
6
|
"actionId": "conn_mod_def::GJ3108Dwmm4::avAMAq7HQtW6PT8JhPg5vA",
|
|
7
7
|
"resultsPath": "",
|
|
8
8
|
"idField": "id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Notion pages and databases — full workspace search with titles, properties, and metadata",
|
|
3
3
|
"platform": "notion",
|
|
4
4
|
"model": "search",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "notion" },
|
|
6
6
|
"actionId": "conn_mod_def::GJ5En67fz04::-CJAS419SVWm7L2l6brp6A",
|
|
7
7
|
"resultsPath": "results",
|
|
8
8
|
"idField": "id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Stripe balance transactions — payments, refunds, payouts, and fees with amount, currency, and status",
|
|
3
3
|
"platform": "stripe",
|
|
4
4
|
"model": "balanceTransactions",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "stripe" },
|
|
6
6
|
"actionId": "conn_mod_def::GGx6clhYjSQ::3kEaM3HQTA2JRfW3DzXC4g",
|
|
7
7
|
"resultsPath": "data",
|
|
8
8
|
"idField": "id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"description": "Stripe customers — customer records with email, name, payment methods, and subscription status",
|
|
3
3
|
"platform": "stripe",
|
|
4
4
|
"model": "customers",
|
|
5
|
-
"
|
|
5
|
+
"connection": { "platform": "stripe" },
|
|
6
6
|
"actionId": "conn_mod_def::GGx6clhYjSQ::customers-list",
|
|
7
7
|
"resultsPath": "data",
|
|
8
8
|
"idField": "id",
|
package/skills/one/SKILL.md
CHANGED
|
@@ -175,6 +175,8 @@ one sync schedule add stripe --every 1h
|
|
|
175
175
|
|
|
176
176
|
**Sync rejects custom actions** — profiles must use passthrough. `sync init` only surfaces passthrough models; `sync run` aborts if the list or enrich action is tagged `custom`. If no passthrough exists, compose a flow instead.
|
|
177
177
|
|
|
178
|
+
**Connections are late-bound** — profiles use `"connection": { "platform": "<name>" }`, not literal `connectionKey` strings. The key is resolved at sync time, so `one add <platform>` (re-auth) doesn't break the profile. For multi-account platforms, add `"tag": "<connection-tag>"` to disambiguate. Don't hardcode connection keys in profiles.
|
|
179
|
+
|
|
178
180
|
**Advanced features** (enrich, transform, exclude, identityKey, hooks, --full-refresh, --where-sql delete, cursor resume): run `one guide sync` for the full reference.
|
|
179
181
|
|
|
180
182
|
## Beyond Single Actions
|
|
@@ -282,12 +282,19 @@ The `if`, `unless`, `condition.expression`, `while.condition`, `transform.expres
|
|
|
282
282
|
"action": {
|
|
283
283
|
"platform": "stripe",
|
|
284
284
|
"actionId": "conn_mod_def::xxx::yyy",
|
|
285
|
-
"
|
|
285
|
+
"connection": { "platform": "stripe" },
|
|
286
286
|
"data": { "query": "email:'{{$.input.customerEmail}}'" }
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
+
**Connection forms.** Each action step sets exactly one of:
|
|
292
|
+
|
|
293
|
+
- **`connection: { platform: "<name>", "tag"?: "<tag>" }`** (preferred) — late-bound, resolved at flow-execute time. Survives re-auth (which always mints a new key). Use `tag` to disambiguate when a platform has multiple connections (e.g. multi-account Gmail). Both `platform` and `tag` accept `$.input.x` selectors so flows can be parameterised per-execution.
|
|
294
|
+
- **`connectionKey: "<literal-or-selector>"`** (legacy) — passes the key string straight through. Still supported for backwards compat, but breaks on re-auth and forces manual edits across every flow that references the stale key. Migrate to `connection` when convenient.
|
|
295
|
+
|
|
296
|
+
The validator rejects an action that sets both forms (or neither) at `flow validate` and `flow execute` time.
|
|
297
|
+
|
|
291
298
|
### `transform` — JS expression (implicit return)
|
|
292
299
|
|
|
293
300
|
```json
|