lua-cli 3.42.0-beta.1 → 3.42.0-beta.3

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.
@@ -12,6 +12,7 @@ import { z } from 'zod';
12
12
  ## Overview
13
13
 
14
14
  Webhooks enable:
15
+
15
16
  - Receive HTTP requests from external services
16
17
  - Validate query parameters, headers, and body
17
18
  - Process events from third-party platforms
@@ -51,11 +52,49 @@ interface LuaWebhookConfig {
51
52
  /** Optional HMAC-SHA256 signing key (see Request Verification) */
52
53
  secret?: string;
53
54
 
55
+ /** Reject schema-violating requests with HTTP 400 (default: log a warning only) */
56
+ enforceSchemas?: boolean;
57
+
54
58
  /** Function that executes the webhook logic */
55
59
  execute: (event: LuaWebhookEvent) => Promise<any>;
56
60
  }
57
61
  ```
58
62
 
63
+ ## Schema Enforcement
64
+
65
+ `querySchema`, `headerSchema` and `bodySchema` are checked before `execute` runs, for webhooks
66
+ compiled with this CLI version or later. Only the schemas you declare are applied. An absent input
67
+ is validated as `{}` (query/headers) or `undefined` (body), so a required `bodySchema` flags a
68
+ request with no body.
69
+
70
+ **Default: log only.** A violating request still reaches `execute` with the original `event`, and a
71
+ single warning is written to the webhook's logs (also printed by `lua test`):
72
+
73
+ ```
74
+ [lua:webhook-schema] would-reject webhook=<name> parts=query,body issues=3 first=body:n:invalid_type (request still processed; ...)
75
+ ```
76
+
77
+ The line names the webhook, the failing parts, the total issue count and the first issue as
78
+ `<part>:<path>:<code>`. It never includes input values or issue messages. Filter the logs on the
79
+ `[lua:webhook-schema] would-reject` prefix to count would-rejects.
80
+
81
+ **Opt in to rejection** with `enforceSchemas: true`. A violating request is then rejected before
82
+ `execute` (query is checked first, then headers, then body) with HTTP 400 when deployed, and fails
83
+ `lua test` with the same message:
84
+
85
+ ```json
86
+ { "statusCode": 400, "code": "VALIDATION", "message": "Invalid body", "part": "body", "issues": [] }
87
+ ```
88
+
89
+ Recommended rollout: deploy, watch the would-reject warnings, fix the schemas or the senders, then
90
+ set `enforceSchemas: true`. Only a strict boolean `true` enforces.
91
+
92
+ - Validation only: `execute` receives the original `event`. No coercion, defaults or key stripping are applied to `event.query`, `event.headers` or `event.body`.
93
+ - Header names are lowercase, so keys in a `headerSchema` must be lowercase (`'x-probe'`, not `'X-Probe'`). Transport headers are present too, so avoid `z.strictObject` for headers.
94
+ - Webhooks that also subscribe to events receive `body: { eventType, ...payload }`; make sure `bodySchema` allows `eventType`.
95
+ - Event deliveries are checked too, and they arrive with EMPTY `query` and `headers`. A `querySchema` or `headerSchema` with required keys, or a `bodySchema` that rejects `eventType`, logs a warning on every event delivery. Only with `enforceSchemas: true` do those deliveries fail, get logged at ERROR and not retried. Keep these schemas permissive, or declare them only on webhooks that are never event-subscribed.
96
+ - Webhooks deployed from older CLI versions keep the old, unvalidated behaviour until they are re-compiled and re-pushed.
97
+
59
98
  ## Request Verification
60
99
 
61
100
  A webhook URL is public. Set `secret` and Lua requires every request to carry
@@ -94,11 +133,22 @@ interface LuaWebhookEvent {
94
133
  /** Request body (JSON or raw data) */
95
134
  body?: any;
96
135
 
136
+ /** Exact unparsed request bytes (utf8) — for verifying vendor signatures */
137
+ rawBody?: string;
138
+
97
139
  /** ISO timestamp when the webhook was received */
98
140
  timestamp: string;
99
141
  }
100
142
  ```
101
143
 
144
+ `rawBody` is the request exactly as it arrived on the wire, as a utf8 string.
145
+ Vendor HMAC schemes (Stripe `t=…,v1=…`, GitHub `sha256=…`, Slack `v0:…`) are
146
+ computed over those bytes, and `JSON.stringify(event.body)` does not reproduce
147
+ them (whitespace, key order and escaping differ), so verify against `rawBody`.
148
+ It is present for JSON requests (`''` for an empty body) and absent for
149
+ form-encoded or plain-text bodies and for webhooks fired by platform events.
150
+ Header keys arrive lowercased. See [Verify Signatures](#verify-signatures).
151
+
102
152
  ## Complete Examples
103
153
 
104
154
  ### GitHub Webhook
@@ -113,22 +163,24 @@ export const githubWebhook = new LuaWebhook({
113
163
 
114
164
  headerSchema: z.object({
115
165
  'x-github-event': z.string(),
116
- 'x-hub-signature': z.string()
166
+ 'x-hub-signature': z.string(),
117
167
  }),
118
168
 
119
169
  bodySchema: z.object({
120
170
  repository: z.object({
121
171
  name: z.string(),
122
- full_name: z.string()
172
+ full_name: z.string(),
123
173
  }),
124
174
  pusher: z.object({
125
175
  name: z.string(),
126
- email: z.string()
176
+ email: z.string(),
127
177
  }),
128
- commits: z.array(z.object({
129
- message: z.string(),
130
- url: z.string()
131
- }))
178
+ commits: z.array(
179
+ z.object({
180
+ message: z.string(),
181
+ url: z.string(),
182
+ })
183
+ ),
132
184
  }),
133
185
 
134
186
  execute: async (event) => {
@@ -138,7 +190,7 @@ export const githubWebhook = new LuaWebhook({
138
190
  if (headers['x-github-event'] !== 'push') {
139
191
  return {
140
192
  success: false,
141
- message: 'Only push events are supported'
193
+ message: 'Only push events are supported',
142
194
  };
143
195
  }
144
196
 
@@ -149,25 +201,27 @@ export const githubWebhook = new LuaWebhook({
149
201
  author: body.pusher.name,
150
202
  message: commit.message,
151
203
  url: commit.url,
152
- timestamp: event.timestamp
204
+ timestamp: event.timestamp,
153
205
  });
154
206
  }
155
207
 
156
208
  // Notify relevant user
157
209
  const user = await User.get({ email: body.pusher.email });
158
210
  if (user) {
159
- await user.send([{
160
- type: 'text',
161
- text: `✅ ${body.commits.length} commits pushed to ${body.repository.name}`
162
- }]);
211
+ await user.send([
212
+ {
213
+ type: 'text',
214
+ text: `✅ ${body.commits.length} commits pushed to ${body.repository.name}`,
215
+ },
216
+ ]);
163
217
  }
164
218
 
165
219
  return {
166
220
  success: true,
167
221
  commits: body.commits.length,
168
- repository: body.repository.name
222
+ repository: body.repository.name,
169
223
  };
170
- }
224
+ },
171
225
  });
172
226
  ```
173
227
 
@@ -182,7 +236,7 @@ export const stripeWebhook = new LuaWebhook({
182
236
  description: 'Handles Stripe payment events',
183
237
 
184
238
  headerSchema: z.object({
185
- 'stripe-signature': z.string()
239
+ 'stripe-signature': z.string(),
186
240
  }),
187
241
 
188
242
  bodySchema: z.object({
@@ -193,9 +247,9 @@ export const stripeWebhook = new LuaWebhook({
193
247
  amount: z.number(),
194
248
  currency: z.string(),
195
249
  customer: z.string().optional(),
196
- metadata: z.record(z.any()).optional()
197
- })
198
- })
250
+ metadata: z.record(z.any()).optional(),
251
+ }),
252
+ }),
199
253
  }),
200
254
 
201
255
  execute: async (event) => {
@@ -214,23 +268,25 @@ export const stripeWebhook = new LuaWebhook({
214
268
  await order.update({
215
269
  paymentId: payment.id,
216
270
  paidAmount: payment.amount / 100,
217
- paidAt: event.timestamp
271
+ paidAt: event.timestamp,
218
272
  });
219
273
 
220
274
  // Notify customer
221
275
  if (payment.customer) {
222
276
  const user = await User.get(payment.customer);
223
- await user.send([{
224
- type: 'text',
225
- text: `✅ Payment received! Your order #${orderId} is being processed.`
226
- }]);
277
+ await user.send([
278
+ {
279
+ type: 'text',
280
+ text: `✅ Payment received! Your order #${orderId} is being processed.`,
281
+ },
282
+ ]);
227
283
  }
228
284
  }
229
285
 
230
286
  return {
231
287
  success: true,
232
288
  event: 'payment_succeeded',
233
- orderId
289
+ orderId,
234
290
  };
235
291
  }
236
292
 
@@ -241,16 +297,18 @@ export const stripeWebhook = new LuaWebhook({
241
297
  // Notify customer of failure
242
298
  if (payment.customer) {
243
299
  const user = await User.get(payment.customer);
244
- await user.send([{
245
- type: 'text',
246
- text: `❌ Payment failed for order #${orderId}. Please try again.`
247
- }]);
300
+ await user.send([
301
+ {
302
+ type: 'text',
303
+ text: `❌ Payment failed for order #${orderId}. Please try again.`,
304
+ },
305
+ ]);
248
306
  }
249
307
 
250
308
  return {
251
309
  success: true,
252
310
  event: 'payment_failed',
253
- orderId
311
+ orderId,
254
312
  };
255
313
  }
256
314
 
@@ -258,10 +316,10 @@ export const stripeWebhook = new LuaWebhook({
258
316
  return {
259
317
  success: true,
260
318
  event: body.type,
261
- message: 'Event received but not handled'
319
+ message: 'Event received but not handled',
262
320
  };
263
321
  }
264
- }
322
+ },
265
323
  });
266
324
  ```
267
325
 
@@ -278,12 +336,14 @@ export const slackWebhook = new LuaWebhook({
278
336
  bodySchema: z.object({
279
337
  type: z.string(),
280
338
  challenge: z.string().optional(),
281
- event: z.object({
282
- type: z.string(),
283
- user: z.string().optional(),
284
- text: z.string().optional(),
285
- channel: z.string()
286
- }).optional()
339
+ event: z
340
+ .object({
341
+ type: z.string(),
342
+ user: z.string().optional(),
343
+ text: z.string().optional(),
344
+ channel: z.string(),
345
+ })
346
+ .optional(),
287
347
  }),
288
348
 
289
349
  execute: async (event) => {
@@ -306,10 +366,9 @@ export const slackWebhook = new LuaWebhook({
306
366
  // Process user message
307
367
  if (slackEvent.type === 'message' && slackEvent.text) {
308
368
  // Generate AI response
309
- const response = await AI.generate(
310
- 'You are a helpful Slack bot assistant.',
311
- [{ type: 'text', text: slackEvent.text }]
312
- );
369
+ const response = await AI.generate('You are a helpful Slack bot assistant.', [
370
+ { type: 'text', text: slackEvent.text },
371
+ ]);
313
372
 
314
373
  // Store interaction
315
374
  await Data.create('slack_messages', {
@@ -317,20 +376,20 @@ export const slackWebhook = new LuaWebhook({
317
376
  channel: slackEvent.channel,
318
377
  message: slackEvent.text,
319
378
  response,
320
- timestamp: event.timestamp
379
+ timestamp: event.timestamp,
321
380
  });
322
381
 
323
382
  // TODO: Send response back to Slack using Slack API
324
383
 
325
384
  return {
326
385
  success: true,
327
- response
386
+ response,
328
387
  };
329
388
  }
330
389
  }
331
390
 
332
391
  return { success: true, message: 'Event received' };
333
- }
392
+ },
334
393
  });
335
394
  ```
336
395
 
@@ -345,7 +404,7 @@ export const contactFormWebhook = new LuaWebhook({
345
404
  description: 'Handles contact form submissions',
346
405
 
347
406
  querySchema: z.object({
348
- source: z.enum(['website', 'mobile_app']).optional()
407
+ source: z.enum(['website', 'mobile_app']).optional(),
349
408
  }),
350
409
 
351
410
  bodySchema: z.object({
@@ -354,7 +413,7 @@ export const contactFormWebhook = new LuaWebhook({
354
413
  phone: z.string().optional(),
355
414
  subject: z.string(),
356
415
  message: z.string().min(10),
357
- consent: z.boolean()
416
+ consent: z.boolean(),
358
417
  }),
359
418
 
360
419
  execute: async (event) => {
@@ -364,7 +423,7 @@ export const contactFormWebhook = new LuaWebhook({
364
423
  if (!body.consent) {
365
424
  return {
366
425
  success: false,
367
- error: 'Consent is required'
426
+ error: 'Consent is required',
368
427
  };
369
428
  }
370
429
 
@@ -377,7 +436,7 @@ export const contactFormWebhook = new LuaWebhook({
377
436
  message: body.message,
378
437
  source: query?.source || 'unknown',
379
438
  submittedAt: event.timestamp,
380
- status: 'new'
439
+ status: 'new',
381
440
  });
382
441
 
383
442
  // Create or update user
@@ -387,22 +446,24 @@ export const contactFormWebhook = new LuaWebhook({
387
446
  await user.update({
388
447
  name: body.name,
389
448
  email: body.email,
390
- phone: body.phone
449
+ phone: body.phone,
391
450
  });
392
451
  }
393
452
 
394
453
  // Send confirmation
395
- await user.send([{
396
- type: 'text',
397
- text: `Thank you for contacting us, ${body.name}! We've received your message about "${body.subject}" and will get back to you soon.`
398
- }]);
454
+ await user.send([
455
+ {
456
+ type: 'text',
457
+ text: `Thank you for contacting us, ${body.name}! We've received your message about "${body.subject}" and will get back to you soon.`,
458
+ },
459
+ ]);
399
460
 
400
461
  return {
401
462
  success: true,
402
463
  submissionId: entry.id,
403
- message: 'Form submitted successfully'
464
+ message: 'Form submitted successfully',
404
465
  };
405
- }
466
+ },
406
467
  });
407
468
  ```
408
469
 
@@ -418,17 +479,17 @@ export const apiWebhook = new LuaWebhook({
418
479
 
419
480
  headerSchema: z.object({
420
481
  'x-api-key': z.string(),
421
- 'content-type': z.literal('application/json')
482
+ 'content-type': z.literal('application/json'),
422
483
  }),
423
484
 
424
485
  querySchema: z.object({
425
486
  action: z.enum(['create', 'update', 'delete']),
426
- entity: z.string()
487
+ entity: z.string(),
427
488
  }),
428
489
 
429
490
  bodySchema: z.object({
430
491
  id: z.string(),
431
- data: z.record(z.any())
492
+ data: z.record(z.any()),
432
493
  }),
433
494
 
434
495
  execute: async (event) => {
@@ -439,7 +500,7 @@ export const apiWebhook = new LuaWebhook({
439
500
  if (headers['x-api-key'] !== validApiKey) {
440
501
  return {
441
502
  success: false,
442
- error: 'Invalid API key'
503
+ error: 'Invalid API key',
443
504
  };
444
505
  }
445
506
 
@@ -449,7 +510,7 @@ export const apiWebhook = new LuaWebhook({
449
510
  await Data.create(query.entity, {
450
511
  externalId: body.id,
451
512
  ...body.data,
452
- syncedAt: event.timestamp
513
+ syncedAt: event.timestamp,
453
514
  });
454
515
  break;
455
516
 
@@ -458,7 +519,7 @@ export const apiWebhook = new LuaWebhook({
458
519
  if (entries.data.length > 0) {
459
520
  await Data.update(query.entity, entries.data[0].id, {
460
521
  ...body.data,
461
- syncedAt: event.timestamp
522
+ syncedAt: event.timestamp,
462
523
  });
463
524
  }
464
525
  break;
@@ -475,9 +536,9 @@ export const apiWebhook = new LuaWebhook({
475
536
  success: true,
476
537
  action: query.action,
477
538
  entity: query.entity,
478
- id: body.id
539
+ id: body.id,
479
540
  };
480
- }
541
+ },
481
542
  });
482
543
  ```
483
544
 
@@ -490,6 +551,7 @@ https://api.heylua.ai/webhook/{agentId}/{webhookName}
490
551
  ```
491
552
 
492
553
  Example:
554
+
493
555
  ```
494
556
  https://api.heylua.ai/webhook/agent_123/github-push
495
557
  ```
@@ -525,7 +587,7 @@ curl -X POST http://localhost:3002/webhook/contact-form \
525
587
 
526
588
  ### ✅ DO
527
589
 
528
- - Always validate input with schemas (query, headers, body)
590
+ - Always declare schemas (query, headers, body), watch the would-reject warnings, then opt in with `enforceSchemas: true`
529
591
  - Verify webhook signatures when available (GitHub, Stripe, etc.)
530
592
  - Return meaningful response objects
531
593
  - Store webhook events for auditing
@@ -536,7 +598,7 @@ curl -X POST http://localhost:3002/webhook/contact-form \
536
598
 
537
599
  ### ❌ DON'T
538
600
 
539
- - Don't skip validation - always use schemas
601
+ - Don't skip validation - declare schemas and enable `enforceSchemas` once the warnings are clean
540
602
  - Don't expose sensitive data in responses
541
603
  - Don't perform long-running operations synchronously
542
604
  - Don't trust incoming data without verification
@@ -551,7 +613,7 @@ export const webhook = new LuaWebhook({
551
613
  description: 'Webhook with error handling',
552
614
 
553
615
  bodySchema: z.object({
554
- data: z.string()
616
+ data: z.string(),
555
617
  }),
556
618
 
557
619
  execute: async (event) => {
@@ -561,7 +623,7 @@ export const webhook = new LuaWebhook({
561
623
 
562
624
  return {
563
625
  success: true,
564
- result
626
+ result,
565
627
  };
566
628
  } catch (error) {
567
629
  // Log error
@@ -569,17 +631,17 @@ export const webhook = new LuaWebhook({
569
631
  webhookName: 'robust-webhook',
570
632
  error: error.message,
571
633
  event: event.body,
572
- timestamp: event.timestamp
634
+ timestamp: event.timestamp,
573
635
  });
574
636
 
575
637
  // Return error response
576
638
  return {
577
639
  success: false,
578
640
  error: error.message,
579
- retry: true // Indicate that the sender should retry
641
+ retry: true, // Indicate that the sender should retry
580
642
  };
581
643
  }
582
- }
644
+ },
583
645
  });
584
646
  ```
585
647
 
@@ -587,32 +649,51 @@ export const webhook = new LuaWebhook({
587
649
 
588
650
  ### Verify Signatures
589
651
 
652
+ Verify a vendor's signature in `execute` against `event.rawBody` — the exact
653
+ bytes the vendor signed — never against a re-serialized `event.body`:
654
+
590
655
  ```typescript
591
656
  import crypto from 'crypto';
657
+ import { LuaWebhook, env } from 'lua-cli';
592
658
 
593
- function verifyGitHubSignature(
594
- body: string,
595
- signature: string,
596
- secret: string
597
- ): boolean {
598
- const hmac = crypto.createHmac('sha256', secret);
599
- const digest = 'sha256=' + hmac.update(body).digest('hex');
600
- return crypto.timingSafeEqual(
601
- Buffer.from(signature),
602
- Buffer.from(digest)
603
- );
659
+ function verifyGitHubSignature(rawBody: string, signature: string, secret: string): boolean {
660
+ const digest = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
661
+ const a = Buffer.from(signature);
662
+ const b = Buffer.from(digest);
663
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
604
664
  }
665
+
666
+ export const githubPush = new LuaWebhook({
667
+ name: 'github-push-verified',
668
+ description: 'GitHub push events, signature-checked',
669
+ execute: async (event) => {
670
+ const signature = event.headers?.['x-hub-signature-256'];
671
+ const secret = env('GITHUB_WEBHOOK_SECRET');
672
+ if (!event.rawBody || !signature || !secret || !verifyGitHubSignature(event.rawBody, signature, secret)) {
673
+ return { success: false, error: 'invalid signature' };
674
+ }
675
+ // Signature OK — event.body is the parsed form of the same bytes.
676
+ return { success: true, ref: event.body.ref };
677
+ },
678
+ });
605
679
  ```
606
680
 
681
+ `lua test webhook` has no wire bytes, so it sets `rawBody` to the compact
682
+ `JSON.stringify(body)` of the test input; pass an explicit one with
683
+ `--input '{"body": {...}, "headers": {...}, "rawBody": "<exact bytes>"}'` to
684
+ exercise a signature check against a captured delivery.
685
+
686
+ To have Lua reject unsigned calls before `execute` runs, use the built-in
687
+ `secret` instead (see [Request Verification](#request-verification)).
688
+
607
689
  ### API Key Validation
608
690
 
609
691
  ```typescript
610
692
  headerSchema: z.object({
611
- 'x-api-key': z.string()
612
- .refine(key => key === process.env.WEBHOOK_API_KEY, {
613
- message: 'Invalid API key'
614
- })
615
- })
693
+ 'x-api-key': z.string().refine((key) => key === process.env.WEBHOOK_API_KEY, {
694
+ message: 'Invalid API key',
695
+ }),
696
+ });
616
697
  ```
617
698
 
618
699
  ## Related APIs
@@ -54,21 +54,21 @@ Config: `name`, `description?`, `inputSchema`, `outputSchema?`, `budget?` (`maxC
54
54
 
55
55
  **The call site places.** Every builder call — `then`, `agentStep`, `specialistStep`, `toolStep`, `map`, `approval`, `waitForSignal`, `sleep`, `sleepUntil`, `foreach`, `parallel`, `branch`, `switch`, `dowhile`, `dountil`, `workflow` — appends **exactly one** entry to the chain where it is called. Containers take `StepRef`s: an inline step object, or a **string** naming an `agentStep`/`specialistStep`/`toolStep` declared elsewhere in the same chain — a string ref inside a container means "declare here, inside me" and does not append a second top-level entry. An id declared but never placed is `WORKFLOW_UNPLACED_STEP` (warning locally, error at push); referenced but never declared is `unknown-step-ref` at `.commit()`.
56
56
 
57
- | Verb | Entry |
58
- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
- | `.then(step, input?)` | Sequential step |
60
- | `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, … })` | Agent turn (`prompt` is a `template(...)`) |
61
- | `.specialistStep(id, { role, prompt, … })` | Ephemeral role on the owning agent (D25) |
62
- | `.toolStep(id, { toolId, input? })` | One tool call — `toolId` must name a `LuaTool` the compiler can see as a tool primitive (declared in its own module and exported, or registered on the agent); a tool object defined inline in the workflow file is not detected and fails `WORKFLOW_TOOL_REF_UNRESOLVED` at compile |
63
- | `.map(descriptors, { id })` | Data reshaping — `id` is **required** once the workflow has ≥ 2 maps (`map-id-required`) |
64
- | `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm — the step runs with the map as its own `input` (the arm node carries `input`, like a `toolStep` with one). Not a `foreach` / loop body: those receive their item / the previous output (`mapping-placement` at `.commit()`) — map the items before the container instead |
65
- | `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
66
- | `.foreach(step, { items?, concurrency?, maxItems?, chunk?, rateLimit? })` | Fan-out over an upstream array. `items` lowers to a `<bodyId>_items` mapping ahead of the entry and takes `fromInit(path)` / `fromStep(id, path)` or an `init(...)` / `step(...).path(...)` ref (LUA-864) — the node is byte-identical whichever form you write; any other descriptor (`value`, `template`, `fromRequest`, `rows`, `fromKnowledge`, a fan-in `fromStep([...])`) is `invalid-envelope` at build: `.map({ '': ... }, { id })` before the foreach instead |
67
- | `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
68
- | `.sleep(ms)` / `.sleepUntil(template)` | Engine-side waits — `sleepUntil` lowers to an `<id>_at` mapping + `sleepUntil{dateFrom}` (D6-r1) |
69
- | `.approval(id, { title, approver, timeoutHours, onTimeout, editable?, editablePaths?, itemsPath?, … })` | Human gate — see [Approvals](../workflows/approvals.md) |
70
- | `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })` | External event via `Workflows.signal` — completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }`; payload keys named like a credential (`token`, `secret`, `apiKey`, `accessToken`, …) are persisted `[REDACTED]` (the `signal-reserved-key` warning says so at push / compose) — see [Reserved payload keys](Workflows.md#reserved-payload-keys) |
71
- | `.workflow(id, ref, input?, { workspace? })` | Child run the parent waits for |
57
+ | Verb | Entry |
58
+ | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `.then(step, input?)` | Sequential step |
60
+ | `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, … })` | Agent turn (`prompt` is a `template(...)`) |
61
+ | `.specialistStep(id, { role, prompt, … })` | Ephemeral role on the owning agent (D25) |
62
+ | `.toolStep(id, { toolId, input? })` | One tool call — `toolId` must name a `LuaTool` the compiler can see as a tool primitive (declared in its own module and exported, or registered on the agent); a tool object defined inline in the workflow file is not detected and fails `WORKFLOW_TOOL_REF_UNRESOLVED` at compile |
63
+ | `.map(descriptors, { id })` | Data reshaping — `id` is **required** once the workflow has ≥ 2 maps (`map-id-required`) |
64
+ | `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm — the step runs with the map as its own `input` (the arm node carries `input`, like a `toolStep` with one). Not a `foreach` / loop body: those receive their item / the previous output (`mapping-placement` at `.commit()`) — map the items before the container instead |
65
+ | `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
66
+ | `.foreach(step, { items?, concurrency?, maxItems?, chunk?, rateLimit? })` | Fan-out over an upstream array. `items` lowers to a `<bodyId>_items` mapping ahead of the entry and takes `fromInit(path)` / `fromStep(id, path)` or an `init(...)` / `step(...).path(...)` ref (LUA-864) — the node is byte-identical whichever form you write; any other descriptor (`value`, `template`, `fromRequest`, `rows`, `fromKnowledge`, a fan-in `fromStep([...])`) is `invalid-envelope` at build: `.map({ '': ... }, { id })` before the foreach instead |
67
+ | `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
68
+ | `.sleep(ms)` / `.sleepUntil(template)` | Engine-side waits — `sleepUntil` lowers to an `<id>_at` mapping + `sleepUntil{dateFrom}` (D6-r1) |
69
+ | `.approval(id, { title, approver, timeoutHours, onTimeout, remindAfterHours?, editable?, editablePaths?, itemsPath?, … })` | Human gate — see [Approvals](../workflows/approvals.md) |
70
+ | `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })` | External event via `Workflows.signal` — completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }`; payload keys named like a credential (`token`, `secret`, `apiKey`, `accessToken`, …) are persisted `[REDACTED]` (the `signal-reserved-key` warning says so at push / compose) — see [Reserved payload keys](Workflows.md#reserved-payload-keys) |
71
+ | `.workflow(id, ref, input?, { workspace? })` | Child run the parent waits for |
72
72
 
73
73
  ### Typed predicates
74
74
 
@@ -30,14 +30,16 @@ The Orders API enables:
30
30
  ```typescript
31
31
  enum OrderStatus {
32
32
  PENDING = 'pending',
33
- PROCESSING = 'processing',
34
- SHIPPED = 'shipped',
35
- DELIVERED = 'delivered',
36
- CANCELLED = 'cancelled',
37
- REFUNDED = 'refunded'
33
+ CONFIRMED = 'confirmed',
34
+ FULFILLED = 'fulfilled',
35
+ CANCELLED = 'cancelled'
38
36
  }
39
37
  ```
40
38
 
39
+ > **Note:** the platform spells the fulfilled status `fullfilled`. `OrderStatus.FULFILLED` is sent to the platform as `fullfilled` (for both `updateStatus` and `get(status)`), and `order.common.status` reads back as `fullfilled`. Any other status value is rejected with HTTP 400, and an unknown order id answers HTTP 404; both are thrown as an `Error` carrying the server message.
40
+ >
41
+ > `Orders.getById`, `updateStatus` and `updateData` take the order `id` (the 24-character hex returned by `create`/`get`), not the `orderId` display string; an id that is not 24-hex answers HTTP 404. An unexpected platform failure on `Orders.create` answers HTTP 424 and a failure on the other calls answers HTTP 500; both are thrown as an `Error` with the generic message `Order request failed`.
42
+
41
43
  ## OrderInstance Methods
42
44
 
43
45
  Once you have an order instance, you can call these methods:
@@ -101,7 +101,7 @@ Searches products by name or description.
101
101
 
102
102
  **Parameters:**
103
103
  - `query` (string) - Search term
104
- - `limit` (number, optional) - Max results (default: 10)
104
+ - `limit` (number, optional) - Max results (default: 5)
105
105
 
106
106
  **Returns:** `Promise<ProductSearchInstance>`
107
107