@withone/cli 1.40.0 → 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.
@@ -811,7 +811,29 @@ async function executeActionStep(step, context, api, permissions, allowedActionI
811
811
  const action = step.action;
812
812
  const platform = resolveValue(action.platform, context);
813
813
  const actionId = resolveValue(action.actionId, context);
814
- const connectionKey = resolveValue(action.connectionKey, context);
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
+ }
815
837
  const data = action.data ? resolveValue(action.data, context) : void 0;
816
838
  const pathVars = action.pathVars ? resolveValue(action.pathVars, context) : void 0;
817
839
  const queryParams = action.queryParams ? resolveValue(action.queryParams, context) : void 0;
@@ -1115,7 +1137,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
1115
1137
  if (flowStack.includes(resolvedKey)) {
1116
1138
  throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
1117
1139
  }
1118
- const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-2KVAPP6E.js");
1140
+ const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-26LNPU4F.js");
1119
1141
  const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
1120
1142
  const subContext = await executeFlow(
1121
1143
  subFlow,
@@ -1661,7 +1683,8 @@ var FLOW_SCHEMA = {
1661
1683
  fields: {
1662
1684
  platform: { type: "string", required: true, description: "Platform name (kebab-case)" },
1663
1685
  actionId: { type: "string", required: true, description: "Action ID from `actions search`" },
1664
- connectionKey: { type: "string", required: true, description: "Connection key (use $.input selector)" },
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." },
1665
1688
  data: { type: "object", required: false, description: "Request body (POST/PUT/PATCH)" },
1666
1689
  pathVars: { type: "object", required: false, description: "URL path variables" },
1667
1690
  queryParams: { type: "object", required: false, description: "Query parameters" },
@@ -1674,7 +1697,7 @@ var FLOW_SCHEMA = {
1674
1697
  action: {
1675
1698
  platform: "stripe",
1676
1699
  actionId: "conn_mod_def::xxx::yyy",
1677
- connectionKey: "$.input.stripeConnectionKey",
1700
+ connection: { platform: "stripe" },
1678
1701
  data: { query: "email:'{{$.input.customerEmail}}'" }
1679
1702
  }
1680
1703
  }
@@ -1723,7 +1746,7 @@ var FLOW_SCHEMA = {
1723
1746
  type: "condition",
1724
1747
  condition: {
1725
1748
  expression: "$.steps.search.response.data.length > 0",
1726
- then: [{ id: "notify", name: "Send notification", type: "action", action: { platform: "slack", actionId: "...", connectionKey: "$.input.slackKey", data: { text: "Found!" } } }],
1749
+ then: [{ id: "notify", name: "Send notification", type: "action", action: { platform: "slack", actionId: "...", connection: { platform: "slack" }, data: { text: "Found!" } } }],
1727
1750
  else: [{ id: "logMiss", name: "Log not found", type: "transform", transform: { expression: "'Not found'" } }]
1728
1751
  }
1729
1752
  }
@@ -1747,7 +1770,7 @@ var FLOW_SCHEMA = {
1747
1770
  loop: {
1748
1771
  over: "$.steps.listOrders.response.data",
1749
1772
  as: "order",
1750
- steps: [{ id: "createInvoice", name: "Create invoice", type: "action", action: { platform: "stripe", actionId: "...", connectionKey: "$.input.stripeKey", data: { amount: "$.loop.order.total" } } }]
1773
+ steps: [{ id: "createInvoice", name: "Create invoice", type: "action", action: { platform: "stripe", actionId: "...", connection: { platform: "stripe" }, data: { amount: "$.loop.order.total" } } }]
1751
1774
  }
1752
1775
  }
1753
1776
  },
@@ -1765,8 +1788,8 @@ var FLOW_SCHEMA = {
1765
1788
  type: "parallel",
1766
1789
  parallel: {
1767
1790
  steps: [
1768
- { id: "getStripe", name: "Get Stripe data", type: "action", action: { platform: "stripe", actionId: "...", connectionKey: "$.input.stripeKey" } },
1769
- { id: "getSlack", name: "Get Slack data", type: "action", action: { platform: "slack", actionId: "...", connectionKey: "$.input.slackKey" } }
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" } } }
1770
1793
  ]
1771
1794
  }
1772
1795
  }
@@ -1818,7 +1841,7 @@ var FLOW_SCHEMA = {
1818
1841
  while: {
1819
1842
  condition: "$.steps.paginate.output.lastResult.nextPageToken != null",
1820
1843
  maxIterations: 50,
1821
- steps: [{ id: "fetchPage", name: "Fetch next page", type: "action", action: { platform: "gmail", actionId: "...", connectionKey: "$.input.gmailKey" } }]
1844
+ steps: [{ id: "fetchPage", name: "Fetch next page", type: "action", action: { platform: "gmail", actionId: "...", connection: { platform: "gmail" } } }]
1822
1845
  }
1823
1846
  }
1824
1847
  },
@@ -1842,7 +1865,7 @@ var FLOW_SCHEMA = {
1842
1865
  configKey: "paginate",
1843
1866
  description: "Auto-paginate API results into a single array",
1844
1867
  fields: {
1845
- 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`)" },
1846
1869
  pageTokenField: { type: "string", required: true, description: "Dot-path in response to next page token" },
1847
1870
  resultsField: { type: "string", required: true, description: "Dot-path in response to results array" },
1848
1871
  inputTokenParam: { type: "string", required: true, description: "Dot-path in action config where page token is injected" },
@@ -1853,7 +1876,7 @@ var FLOW_SCHEMA = {
1853
1876
  name: "Fetch all Gmail messages",
1854
1877
  type: "paginate",
1855
1878
  paginate: {
1856
- action: { platform: "gmail", actionId: "...", connectionKey: "$.input.gmailKey", queryParams: { maxResults: 100 } },
1879
+ action: { platform: "gmail", actionId: "...", connection: { platform: "gmail" }, queryParams: { maxResults: 100 } },
1857
1880
  pageTokenField: "nextPageToken",
1858
1881
  resultsField: "messages",
1859
1882
  inputTokenParam: "queryParams.pageToken",
@@ -2051,12 +2074,6 @@ The only differences: (1) prepend the stdin-read line, (2) replace \`return X\`
2051
2074
  "description": "What this flow does",
2052
2075
  "version": "1",
2053
2076
  "inputs": {
2054
- "connectionKey": {
2055
- "type": "string",
2056
- "required": true,
2057
- "description": "Platform connection key",
2058
- "connection": { "platform": "stripe" }
2059
- },
2060
2077
  "param": {
2061
2078
  "type": "string",
2062
2079
  "required": true,
@@ -2071,7 +2088,7 @@ The only differences: (1) prepend the stdin-read line, (2) replace \`return X\`
2071
2088
  "action": {
2072
2089
  "platform": "stripe",
2073
2090
  "actionId": "conn_mod_def::xxx::yyy",
2074
- "connectionKey": "$.input.connectionKey",
2091
+ "connection": { "platform": "stripe" },
2075
2092
  "data": { "query": "{{$.input.param}}" }
2076
2093
  }
2077
2094
  }
@@ -2165,13 +2182,13 @@ Pipes can be applied to any value (objects/arrays are JSON-stringified first for
2165
2182
 
2166
2183
  ### When to use bare selectors vs \`{{...}}\` interpolation
2167
2184
 
2168
- - **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).
2169
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.
2170
2187
  - **Rule of thumb**: If the value is purely a selector, use bare. If it's a string containing a selector, use \`{{...}}\`.
2171
2188
 
2172
2189
  ### Selectors vs expressions
2173
2190
 
2174
- 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:
2175
2192
 
2176
2193
  \`\`\`json
2177
2194
  { "inputs": { "maxResults": { "type": "number", "default": 10 } } }
@@ -2291,9 +2308,30 @@ A \`flow\` step's \`flow.key\` accepts selectors and Handlebars interpolations,
2291
2308
 
2292
2309
  Conditional execution: \`"if": "$.steps.prev.response.data.length > 0"\`
2293
2310
 
2294
- ## Input Connection Auto-Resolution
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"\`.
2327
+
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)
2295
2333
 
2296
- When an input has \`"connection": { "platform": "stripe" }\`, the flow engine can automatically resolve the connection key at execution time. If the user has exactly one connection for that platform, the engine fills in the key without requiring \`-i connectionKey=...\`. If multiple connections exist, the user must specify which one. This is metadata for tooling \u2014 it does not affect the flow JSON structure, but it makes execution more convenient.
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.
2297
2335
 
2298
2336
  ## Complete Example: Fetch Data, Transform, Notify
2299
2337
 
@@ -2304,18 +2342,6 @@ When an input has \`"connection": { "platform": "stripe" }\`, the flow engine ca
2304
2342
  "description": "Fetch recent contacts from CRM, build a summary, post to Slack",
2305
2343
  "version": "1",
2306
2344
  "inputs": {
2307
- "crmConnectionKey": {
2308
- "type": "string",
2309
- "required": true,
2310
- "description": "CRM platform connection key",
2311
- "connection": { "platform": "attio" }
2312
- },
2313
- "slackConnectionKey": {
2314
- "type": "string",
2315
- "required": true,
2316
- "description": "Slack connection key",
2317
- "connection": { "platform": "slack" }
2318
- },
2319
2345
  "slackChannel": {
2320
2346
  "type": "string",
2321
2347
  "required": true,
@@ -2330,7 +2356,7 @@ When an input has \`"connection": { "platform": "stripe" }\`, the flow engine ca
2330
2356
  "action": {
2331
2357
  "platform": "attio",
2332
2358
  "actionId": "ATTIO_LIST_PEOPLE_ACTION_ID",
2333
- "connectionKey": "$.input.crmConnectionKey",
2359
+ "connection": { "platform": "attio" },
2334
2360
  "queryParams": { "limit": "10" }
2335
2361
  }
2336
2362
  },
@@ -2349,7 +2375,7 @@ When an input has \`"connection": { "platform": "stripe" }\`, the flow engine ca
2349
2375
  "action": {
2350
2376
  "platform": "slack",
2351
2377
  "actionId": "SLACK_SEND_MESSAGE_ACTION_ID",
2352
- "connectionKey": "$.input.slackConnectionKey",
2378
+ "connection": { "platform": "slack" },
2353
2379
  "data": {
2354
2380
  "channel": "$.input.slackChannel",
2355
2381
  "text": "{{$.steps.buildSummary.output.summary}}"
@@ -11,7 +11,7 @@ import {
11
11
  saveFlow,
12
12
  summarizeFlowInputs,
13
13
  walkSteps
14
- } from "./chunk-SSEDFOVV.js";
14
+ } from "./chunk-A5AFLKCQ.js";
15
15
  export {
16
16
  FlowRunner,
17
17
  collectStepTypes,
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  resolveFlowPath,
20
20
  saveFlow,
21
21
  validateActionInput
22
- } from "./chunk-SSEDFOVV.js";
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
- if (!a.connectionKey) errors.push({ path: `${fieldPath}.connectionKey`, message: 'Action must have "connectionKey"' });
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
- connectionKey: "$.input.connectionKey",
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
- connectionKey: "$.input.connectionKey"
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
- connectionKey: "$.input.connectionKey"
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
- connectionKey: "$.input.connectionKey"
4264
+ connection: { platform: "PLATFORM_NAME" }
4253
4265
  }
4254
4266
  },
4255
4267
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -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
- "connectionKey": "$.input.stripeConnectionKey",
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