@shipfox/workflow-document 3.1.0 → 3.2.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.
@@ -52,6 +52,8 @@ export const WORKFLOW_DOCUMENT_JOB_OUTPUTS_MAX_ENTRIES = WORKFLOW_DOCUMENT_ENV_M
52
52
  export const WORKFLOW_DOCUMENT_STEP_OUTPUTS_MAX_ENTRIES = WORKFLOW_DOCUMENT_ENV_MAX_ENTRIES;
53
53
  export const WORKFLOW_DOCUMENT_STEP_OUTPUT_SCHEMA_MAX_SERIALIZED_BYTES = WORKFLOW_DOCUMENT_ENV_MAX_SERIALIZED_BYTES;
54
54
  export const WORKFLOW_DOCUMENT_STEP_OUTPUT_SCHEMA_MAX_DEPTH = 64;
55
+ export const WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES = 32 * 1024;
56
+ export const WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH = 16;
55
57
  const utf8Encoder = new TextEncoder();
56
58
  export const workflowDocumentEnvSchema = z.record(envNameSchema, z.union([
57
59
  envStringValueSchema,
@@ -76,10 +78,186 @@ export const workflowDocumentEnvSchema = z.record(envNameSchema, z.union([
76
78
  description: `Environment variables as string, number, or boolean values. Each map allows up to ${WORKFLOW_DOCUMENT_ENV_MAX_ENTRIES} entries and ${WORKFLOW_DOCUMENT_ENV_MAX_SERIALIZED_BYTES} serialized bytes.`
77
79
  });
78
80
  const workflowDocumentStepOutputKeyPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
81
+ export const workflowDocumentToolStepWithSchema = z// Validate nested values in an iterative refinement. A recursive Zod schema
82
+ // would traverse hostile depth before the tool-input limits can reject it.
83
+ .record(z.string().min(1), z.unknown()).superRefine((withValue, ctx)=>{
84
+ // The server injects `method` for `family.method` tools, so the author can
85
+ // never set it.
86
+ if ('method' in withValue) {
87
+ ctx.addIssue({
88
+ code: 'custom',
89
+ path: [
90
+ 'method'
91
+ ],
92
+ message: '`method` is not a valid tool input; the server injects it for `family.method` tools.'
93
+ });
94
+ }
95
+ validateWorkflowDocumentToolWith(withValue, ctx);
96
+ }).transform((withValue)=>withValue).meta({
97
+ description: 'Tool inputs as a JSON tree. The map allows up to ' + WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES + ' serialized bytes and ' + WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH + ' nesting levels. Tool steps are not available yet.'
98
+ });
99
+ function validateWorkflowDocumentToolWith(withValue, ctx) {
100
+ const activeObjects = new Set();
101
+ const tasks = [
102
+ {
103
+ kind: 'value',
104
+ value: withValue,
105
+ depth: 1,
106
+ path: []
107
+ }
108
+ ];
109
+ let serializedBytes = 0;
110
+ const addSerializedBytes = (byteLength)=>{
111
+ serializedBytes += byteLength;
112
+ if (serializedBytes <= WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES) return true;
113
+ ctx.addIssue({
114
+ code: 'custom',
115
+ message: `Tool \`with\` cannot serialize to more than ${WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES} bytes.`
116
+ });
117
+ return false;
118
+ };
119
+ while(tasks.length > 0){
120
+ const task = tasks.pop();
121
+ if (task === undefined) continue;
122
+ if (task.kind === 'bytes') {
123
+ if (!addSerializedBytes(task.byteLength)) return;
124
+ continue;
125
+ }
126
+ if (task.kind === 'end') {
127
+ activeObjects.delete(task.value);
128
+ if (!addSerializedBytes(task.byteLength)) return;
129
+ continue;
130
+ }
131
+ const { value, depth, path } = task;
132
+ if (value === null) {
133
+ if (!addSerializedBytes(4)) return;
134
+ continue;
135
+ }
136
+ if (typeof value === 'string' || typeof value === 'boolean') {
137
+ if (!addSerializedBytes(jsonPrimitiveByteLength(value))) return;
138
+ continue;
139
+ }
140
+ if (typeof value === 'number') {
141
+ if (!Number.isFinite(value)) {
142
+ ctx.addIssue({
143
+ code: 'custom',
144
+ path,
145
+ message: 'Tool `with` values must be JSON-compatible.'
146
+ });
147
+ return;
148
+ }
149
+ if (!addSerializedBytes(jsonPrimitiveByteLength(value))) return;
150
+ continue;
151
+ }
152
+ if (typeof value !== 'object' || value === null) {
153
+ ctx.addIssue({
154
+ code: 'custom',
155
+ path,
156
+ message: 'Tool `with` values must be JSON-compatible.'
157
+ });
158
+ return;
159
+ }
160
+ if (depth > WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH) {
161
+ ctx.addIssue({
162
+ code: 'custom',
163
+ message: `Tool \`with\` cannot be nested deeper than ${WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH} levels.`
164
+ });
165
+ return;
166
+ }
167
+ if (activeObjects.has(value)) {
168
+ ctx.addIssue({
169
+ code: 'custom',
170
+ path,
171
+ message: 'Tool `with` values must be a JSON tree.'
172
+ });
173
+ return;
174
+ }
175
+ activeObjects.add(value);
176
+ if (Array.isArray(value)) {
177
+ if (!addSerializedBytes(1)) return;
178
+ tasks.push({
179
+ kind: 'end',
180
+ value,
181
+ byteLength: 1
182
+ });
183
+ for(let index = value.length - 1; index >= 0; index -= 1){
184
+ tasks.push({
185
+ kind: 'value',
186
+ value: value[index],
187
+ depth: depth + 1,
188
+ path: [
189
+ ...path,
190
+ index
191
+ ]
192
+ });
193
+ if (index > 0) tasks.push({
194
+ kind: 'bytes',
195
+ byteLength: 1
196
+ });
197
+ }
198
+ continue;
199
+ }
200
+ if (!isJsonRecord(value)) {
201
+ ctx.addIssue({
202
+ code: 'custom',
203
+ path,
204
+ message: 'Tool `with` values must be a JSON tree.'
205
+ });
206
+ return;
207
+ }
208
+ if (!addSerializedBytes(1)) return;
209
+ tasks.push({
210
+ kind: 'end',
211
+ value,
212
+ byteLength: 1
213
+ });
214
+ const entries = Object.entries(value);
215
+ for(let index = entries.length - 1; index >= 0; index -= 1){
216
+ const entry = entries[index];
217
+ if (entry === undefined) continue;
218
+ const [key, child] = entry;
219
+ tasks.push({
220
+ kind: 'value',
221
+ value: child,
222
+ depth: depth + 1,
223
+ path: [
224
+ ...path,
225
+ key
226
+ ]
227
+ });
228
+ tasks.push({
229
+ kind: 'bytes',
230
+ byteLength: jsonPrimitiveByteLength(key) + 1
231
+ });
232
+ if (index > 0) tasks.push({
233
+ kind: 'bytes',
234
+ byteLength: 1
235
+ });
236
+ }
237
+ }
238
+ }
239
+ function jsonPrimitiveByteLength(value) {
240
+ return utf8Encoder.encode(JSON.stringify(value)).byteLength;
241
+ }
242
+ function jsonSerializedByteLength(value) {
243
+ try {
244
+ const serialized = JSON.stringify(value);
245
+ return serialized === undefined ? undefined : utf8Encoder.encode(serialized).byteLength;
246
+ } catch {
247
+ return undefined;
248
+ }
249
+ }
250
+ function isJsonRecord(value) {
251
+ const prototype = Object.getPrototypeOf(value);
252
+ return prototype === Object.prototype || prototype === null;
253
+ }
79
254
  const workflowDocumentStepOutputTypeSchema = z.enum(workflowDocumentStepOutputTypes).meta({
80
255
  description: 'Declared output type. Use `json` when the output has a JSON Schema.'
81
256
  });
82
- const workflowDocumentStepOutputDeclarationSchema = z.union([
257
+ const workflowDocumentToolStepOutputMappingValueSchema = z.string().min(1).refine((value)=>value.includes('$' + '{{'), {
258
+ message: 'Tool-step output mappings must use a $' + '{{ }} expression.'
259
+ });
260
+ export const workflowDocumentStepOutputDeclarationSchema = z.union([
83
261
  workflowDocumentStepOutputTypeSchema.transform((type)=>({
84
262
  type
85
263
  })),
@@ -112,7 +290,17 @@ const workflowDocumentStepOutputDeclarationSchema = z.union([
112
290
  });
113
291
  return;
114
292
  }
115
- const serializedBytes = utf8Encoder.encode(JSON.stringify(schema)).byteLength;
293
+ const serializedBytes = jsonSerializedByteLength(schema);
294
+ if (serializedBytes === undefined) {
295
+ ctx.addIssue({
296
+ code: 'custom',
297
+ path: [
298
+ 'schema'
299
+ ],
300
+ message: 'Schema must be a serializable JSON Schema document.'
301
+ });
302
+ return;
303
+ }
116
304
  if (serializedBytes > WORKFLOW_DOCUMENT_STEP_OUTPUT_SCHEMA_MAX_SERIALIZED_BYTES) {
117
305
  ctx.addIssue({
118
306
  code: 'custom',
@@ -133,7 +321,12 @@ const workflowDocumentStepOutputDeclarationSchema = z.union([
133
321
  });
134
322
  }
135
323
  });
136
- export const workflowDocumentStepOutputsSchema = z.record(z.string(), workflowDocumentStepOutputDeclarationSchema).superRefine((outputs, ctx)=>{
324
+ function stepOutputsAreMappingForm(outputs) {
325
+ const interpolationOpen = '$' + '{{';
326
+ const values = Object.values(outputs);
327
+ return values.some((value)=>typeof value === 'string' && value.includes(interpolationOpen));
328
+ }
329
+ function stepOutputsRecordChecks(outputs, ctx) {
137
330
  const entries = Object.keys(outputs).length;
138
331
  if (entries > WORKFLOW_DOCUMENT_STEP_OUTPUTS_MAX_ENTRIES) {
139
332
  ctx.addIssue({
@@ -151,21 +344,38 @@ export const workflowDocumentStepOutputsSchema = z.record(z.string(), workflowDo
151
344
  message: 'Output keys must be CEL identifiers.'
152
345
  });
153
346
  }
154
- }).meta({
347
+ }
348
+ export const workflowDocumentStepOutputsSchema = z.record(z.string(), workflowDocumentStepOutputDeclarationSchema).superRefine((outputs, ctx)=>stepOutputsRecordChecks(outputs, ctx)).meta({
155
349
  description: `Named step outputs. Keys must be CEL identifiers and each step allows up to ${WORKFLOW_DOCUMENT_STEP_OUTPUTS_MAX_ENTRIES} declarations.`
156
350
  });
351
+ // The tool-step `outputs` form maps output keys to a single `${{ }}` expression
352
+ // over `result`. The expression layer validates the interpolation; the mapping
353
+ // form is rejected with the reserved tool step fields until tool steps exist.
354
+ export const workflowDocumentToolStepOutputsSchema = z.record(z.string(), workflowDocumentToolStepOutputMappingValueSchema).superRefine((outputs, ctx)=>stepOutputsRecordChecks(outputs, ctx)).meta({
355
+ description: 'Tool-step output mappings over `result`. Each value is exactly one $' + '{{ }} expression. Tool steps are not available yet.'
356
+ });
357
+ // `outputs` carries the declaration form on run, agent, and checkout steps and
358
+ // the expression mapping form on tool steps. One value union accepts both so a
359
+ // reserved tool step parses and is rejected by the step `superRefine`; zod
360
+ // reports the declaration branch's own issues for malformed declarations, and
361
+ // the step `superRefine` rejects the mapping form on every other step kind.
362
+ const workflowDocumentStepOutputValueSchema = z.union([
363
+ workflowDocumentStepOutputDeclarationSchema,
364
+ workflowDocumentToolStepOutputMappingValueSchema
365
+ ]);
366
+ const workflowDocumentStepOutputsFieldSchema = z.record(z.string(), workflowDocumentStepOutputValueSchema).superRefine((outputs, ctx)=>stepOutputsRecordChecks(outputs, ctx));
157
367
  const workflowDocumentTriggerBaseSchema = {
158
368
  source: z.string().min(1).meta({
159
- description: 'Integration connection slug or built-in trigger source. See [Trigger sources](/reference/trigger-sources).'
369
+ description: 'Integration connection slug or built-in trigger source. See [Integrations](/integrations) for provider sources.'
160
370
  }),
161
371
  with: z.record(z.string(), z.unknown()).optional().meta({
162
- description: 'Provider-specific values used to match or configure the trigger. See [Trigger sources](/reference/trigger-sources).'
372
+ description: 'Provider-specific values used to match or configure the trigger. See the provider event catalog in [Integrations](/integrations).'
163
373
  }),
164
374
  filter: z.string().min(1).optional().meta({
165
375
  description: 'CEL condition that filters matching events. It is not supported for `manual` or `cron` triggers. See [Expressions](/reference/expressions) and [Contexts](/reference/contexts#context-availability).'
166
376
  }),
167
377
  config: z.record(z.string(), z.unknown()).optional().meta({
168
- description: 'Source-specific configuration. It is supported only for top-level triggers with a known built-in source. See [cron triggers](/reference/trigger-sources#cron).'
378
+ description: 'Source-specific configuration. It is supported only for top-level triggers with a known built-in source. See [Schedule workflows](/how-to/author-workflows/schedule-workflows).'
169
379
  })
170
380
  };
171
381
  export const triggerSourceConfigSchemas = {
@@ -368,8 +578,11 @@ export const workflowDocumentAgentStepFields = [
368
578
  // payload keys are present and emits one targeted issue per failure mode (a
369
579
  // plain union would surface every branch's errors at once). The `agent`
370
580
  // keyword is declared only so the reserved-keyword case produces a clear
371
- // message instead of a generic "unrecognized key".
372
- export const workflowDocumentStepSchema = z.strictObject({
581
+ // message instead of a generic "unrecognized key". The `tool`, `connection`,
582
+ // `with`, and tool-step `outputs` mapping form are declared the same way: they
583
+ // parse so their shape is checked, then any step carrying a reserved tool field
584
+ // is rejected until the tool step kind exists.
585
+ const workflowDocumentStepBaseSchema = z.strictObject({
373
586
  key: z.string().min(1).optional().meta({
374
587
  description: 'Stable step key for dependencies and outputs.'
375
588
  }),
@@ -410,16 +623,24 @@ export const workflowDocumentStepSchema = z.strictObject({
410
623
  agent: z.unknown().optional().meta({
411
624
  description: 'Reserved keyword. It is rejected; use `prompt` to define an agent step.'
412
625
  }),
626
+ tool: z.string().min(1).optional().meta({
627
+ description: 'Literal integration tool id for a tool step. It is rejected; tool steps are not available yet.'
628
+ }),
629
+ connection: z.string().min(1).optional().meta({
630
+ description: 'Literal integration connection slug for a tool step. It is rejected; tool steps are not available yet.'
631
+ }),
632
+ with: workflowDocumentToolStepWithSchema.optional(),
413
633
  gate: workflowDocumentStepGateSchema.optional().meta({
414
634
  description: 'Success gate and optional restart behavior after the step runs.'
415
635
  }),
416
636
  env: workflowDocumentEnvSchema.optional().meta({
417
637
  description: 'Environment variables for a run step. They are not valid on an agent step.'
418
638
  }),
419
- outputs: workflowDocumentStepOutputsSchema.optional().meta({
420
- description: 'Named output declarations produced by this step.'
639
+ outputs: workflowDocumentStepOutputsFieldSchema.optional().meta({
640
+ description: 'Named output declarations produced by this step, or on a tool step a mapping of output keys to exactly one $' + '{{ }} expression over `result`. Tool steps are not available yet.'
421
641
  })
422
- }).superRefine((step, ctx)=>{
642
+ });
643
+ export const workflowDocumentStepSchema = workflowDocumentStepBaseSchema.superRefine((step, ctx)=>{
423
644
  if (step.agent !== undefined) {
424
645
  ctx.addIssue({
425
646
  code: 'custom',
@@ -430,6 +651,26 @@ export const workflowDocumentStepSchema = z.strictObject({
430
651
  });
431
652
  return;
432
653
  }
654
+ const reservedToolField = step.tool !== undefined ? 'tool' : step.connection !== undefined ? 'connection' : undefined;
655
+ if (reservedToolField !== undefined || step.with !== undefined) {
656
+ ctx.addIssue({
657
+ code: 'custom',
658
+ path: [
659
+ reservedToolField ?? 'with'
660
+ ],
661
+ message: 'Tool steps are not available yet.'
662
+ });
663
+ return;
664
+ }
665
+ if (step.outputs !== undefined && stepOutputsAreMappingForm(step.outputs)) {
666
+ ctx.addIssue({
667
+ code: 'custom',
668
+ path: [
669
+ 'outputs'
670
+ ],
671
+ message: 'The `outputs` mapping form is reserved for tool steps.'
672
+ });
673
+ }
433
674
  if (step.checkout !== undefined) {
434
675
  if (step.run !== undefined) {
435
676
  ctx.addIssue({
@@ -502,7 +743,10 @@ export const workflowDocumentStepSchema = z.strictObject({
502
743
  message: 'An agent step requires "prompt".'
503
744
  });
504
745
  }
505
- });
746
+ }).transform(({ outputs, ...step })=>outputs === undefined ? step : {
747
+ ...step,
748
+ outputs: outputs
749
+ });
506
750
  const workflowDocumentJobOutputsSchema = nonEmptyRecordSchema(z.string().min(1)).superRefine((outputs, ctx)=>{
507
751
  const entries = Object.keys(outputs).length;
508
752
  if (entries > WORKFLOW_DOCUMENT_JOB_OUTPUTS_MAX_ENTRIES) {
@@ -565,14 +809,41 @@ export const workflowDocumentSchema = z.strictObject({
565
809
  })
566
810
  });
567
811
  function maxJsonDepth(value) {
568
- if (value === null || typeof value !== 'object') return 0;
569
- if (Array.isArray(value)) {
570
- if (value.length === 0) return 1;
571
- return 1 + Math.max(...value.map(maxJsonDepth));
812
+ let maximumDepth = 0;
813
+ const activeObjects = new Set();
814
+ const pending = [
815
+ {
816
+ kind: 'enter',
817
+ value,
818
+ depth: 0
819
+ }
820
+ ];
821
+ while(pending.length > 0){
822
+ const current = pending.pop();
823
+ if (current === undefined) continue;
824
+ if (current.kind === 'leave') {
825
+ activeObjects.delete(current.value);
826
+ continue;
827
+ }
828
+ if (current.value === null || typeof current.value !== 'object') continue;
829
+ if (activeObjects.has(current.value)) continue;
830
+ activeObjects.add(current.value);
831
+ const depth = current.depth + 1;
832
+ maximumDepth = Math.max(maximumDepth, depth);
833
+ pending.push({
834
+ kind: 'leave',
835
+ value: current.value
836
+ });
837
+ const children = Object.values(current.value);
838
+ for(let index = children.length - 1; index >= 0; index -= 1){
839
+ pending.push({
840
+ kind: 'enter',
841
+ value: children[index],
842
+ depth
843
+ });
844
+ }
572
845
  }
573
- const entries = Object.values(value);
574
- if (entries.length === 0) return 1;
575
- return 1 + Math.max(...entries.map(maxJsonDepth));
846
+ return maximumDepth;
576
847
  }
577
848
  function isJsonSchemaDocument(value) {
578
849
  return typeof value === 'boolean' || typeof value === 'object' && value !== null && !Array.isArray(value);