lua-cli 3.30.0 → 3.32.1

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.
Files changed (62) hide show
  1. package/dist/api-exports.d.ts +1096 -41
  2. package/dist/api-exports.js +5544 -137
  3. package/dist/api-exports.js.map +1 -1
  4. package/dist/index.js +21255 -8412
  5. package/dist/index.js.map +1 -1
  6. package/dist/voice/test/index.d.ts +4 -4
  7. package/dist/workflow-builder.d.ts +800 -0
  8. package/dist/workflow-builder.js +6273 -0
  9. package/dist/workflow-builder.js.map +1 -0
  10. package/docs/API_INDEX.md +2 -0
  11. package/docs/README.md +27 -9
  12. package/docs/api/Jobs.md +10 -10
  13. package/docs/api/LuaWorkflow.md +89 -0
  14. package/docs/api/Workflows.md +111 -0
  15. package/docs/workflows/approvals.md +41 -0
  16. package/docs/workflows/artefacts-and-datasets.md +19 -0
  17. package/docs/workflows/coding-harness.md +12 -0
  18. package/docs/workflows/compliance-gates.md +16 -0
  19. package/docs/workflows/connections-in-coding-turns.md +10 -0
  20. package/docs/workflows/correlation-keys.md +11 -0
  21. package/docs/workflows/env-overlays.md +12 -0
  22. package/docs/workflows/evidence-bundles.md +11 -0
  23. package/docs/workflows/exports.md +5 -0
  24. package/docs/workflows/external-content-and-toolscope.md +11 -0
  25. package/docs/workflows/git-credentials.md +32 -0
  26. package/docs/workflows/goals.md +46 -0
  27. package/docs/workflows/knowledge-bindings.md +13 -0
  28. package/docs/workflows/limits.md +11 -0
  29. package/docs/workflows/long-steps-and-checkpoints.md +13 -0
  30. package/docs/workflows/migrating-cloud-tasks.md +9 -0
  31. package/docs/workflows/migrating-runs.md +13 -0
  32. package/docs/workflows/output-visibility.md +9 -0
  33. package/docs/workflows/per-item-approvals.md +9 -0
  34. package/docs/workflows/private-network-sources.md +12 -0
  35. package/docs/workflows/recovery.md +32 -0
  36. package/docs/workflows/replay-local.md +35 -0
  37. package/docs/workflows/reply-channels.md +11 -0
  38. package/docs/workflows/retention-and-archival.md +82 -0
  39. package/docs/workflows/roles.md +12 -0
  40. package/docs/workflows/schedules.md +26 -0
  41. package/docs/workflows/script-form.md +50 -0
  42. package/docs/workflows/testing-offline.md +49 -0
  43. package/docs/workflows/workspace-backends.md +11 -0
  44. package/docs/workflows/workspaces-and-long-steps.md +29 -0
  45. package/package.json +8 -3
  46. package/scripts/run-api-extractor.mjs +1 -1
  47. package/template/.gitignore +2 -0
  48. package/template/examples/workflows/CLAUDE.md +27 -0
  49. package/template/examples/workflows/adversarial-verify.workflow.script.js +48 -0
  50. package/template/examples/workflows/github-review.webhook.ts +19 -0
  51. package/template/examples/workflows/linear-ready.trigger.ts +21 -0
  52. package/template/examples/workflows/outreach.ts +55 -0
  53. package/template/examples/workflows/pr-review-round.ts +75 -0
  54. package/template/examples/workflows/provision-tenant.ts +35 -0
  55. package/template/examples/workflows/refund-approval.ts +57 -0
  56. package/template/examples/workflows/research-brief.ts +42 -0
  57. package/template/examples/workflows/reviewed-brief.ts +19 -0
  58. package/template/examples/workflows/support-triage.ts +81 -0
  59. package/template/examples/workflows/ticket-to-pr.ts +137 -0
  60. package/template/examples/workflows/vendor-invoices.ts +83 -0
  61. package/template/lua.skill.yaml +1 -0
  62. package/template/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.30.0",
3
+ "version": "3.32.1",
4
4
  "description": "Build, test, and deploy AI agents with custom tools, webhooks, and scheduled jobs. Features LuaAgent unified configuration, streaming chat, and batch deployment.",
5
5
  "readmeFilename": "README.md",
6
6
  "main": "dist/api-exports.js",
@@ -21,6 +21,10 @@
21
21
  "./voice/test": {
22
22
  "types": "./dist/voice/test/index.d.ts",
23
23
  "default": "./dist/voice/test/index.js"
24
+ },
25
+ "./workflow-builder": {
26
+ "types": "./dist/workflow-builder.d.ts",
27
+ "default": "./dist/workflow-builder.js"
24
28
  }
25
29
  },
26
30
  "keywords": [
@@ -109,9 +113,10 @@
109
113
  "stripe": "^19.2.0",
110
114
  "ts-node": "^10.9.2",
111
115
  "tsup": "^8.5.1",
116
+ "@lua/shared-sandbox": "0.0.1",
117
+ "@lua/workflow-graph": "0.0.1",
112
118
  "@lua/shared-source-sync": "0.0.1",
113
- "@lua/shared-types": "0.0.1",
114
- "@lua/shared-sandbox": "0.0.1"
119
+ "@lua/shared-types": "0.0.1"
115
120
  },
116
121
  "scripts": {
117
122
  "clean": "rm -rf dist temp",
@@ -41,7 +41,7 @@ const configDir = path.join(packageRoot, 'api-extractor');
41
41
  const tempDtsDir = path.join(packageRoot, 'temp', 'dts');
42
42
  const distDir = path.join(packageRoot, 'dist');
43
43
 
44
- const rolledEntries = ['api-exports', 'voice-test'];
44
+ const rolledEntries = ['api-exports', 'voice-test', 'workflow-builder'];
45
45
 
46
46
  const rawEntries = [
47
47
  { src: 'index.d.ts', dest: 'index.d.ts' },
@@ -19,3 +19,5 @@ node_modules/
19
19
  # OS files
20
20
  .DS_Store
21
21
  Thumbs.db
22
+ # Workflows v1 (wave-5 gate): the script-form example IS tracked (WB-09, WF-527)
23
+ !examples/workflows/*.workflow.script.js
@@ -0,0 +1,27 @@
1
+ # Workflow examples
2
+
3
+ Worked examples from workflows-spec 03 §3.2 (the spec is normative — do not restyle them). A file whose header carries a
4
+ `LUA-635:` note departs from the spec's listing on purpose: the compiler is what ships, and the listing was wrong against
5
+ it (a `.then(createStep)` placement used as a string-ref target; a `template(…)` approval title). The note says what
6
+ changed and why; `tests/__tests__/compile.workflow.examples.test.ts` compiles every file here, so a drift cannot return.
7
+
8
+ | File | Shows |
9
+ | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | `research-brief.ts` | (a) sequential + parallel research, typed predicates (`stepOf`/`gt`/`lit`), switch + otherwise |
11
+ | `outreach.ts` | (b) `foreach` + editable approval + an exactly-once send (`ctx.once`), cron schedule, `concurrencyPolicy` |
12
+ | `provision-tenant.ts` | (c) nested workflow + `sleep` + `waitForSignal` |
13
+ | `reviewed-brief.ts` | (d) `specialistStep` — an ephemeral reviewer role on the owning agent (D25) |
14
+ | `refund-approval.ts` | (e) maker-checker + four-eyes + business-hours escalation chain + a recoverable external step (`onError:'park'`) |
15
+ | `ticket-to-pr.ts` + `pr-review-round.ts` + `linear-ready.trigger.ts` + `github-review.webhook.ts` | (f) the SWE headline: Job tier, worktree arms + merge, review loop over a child workflow, trigger `startWorkflow`, webhook → `Workflows.signal` |
16
+ | `support-triage.ts` | (g) knowledge grounding, mandatory `toolScope` on external content, dataset rows, `ctx.artefacts` |
17
+ | `adversarial-verify.workflow.script.js` | (i) script form — `step()` inline effect + a library `role:{ref}` |
18
+ | `vendor-invoices.ts` | (j) per-item approvals (`itemsPath`), `env.template()` overlays, `outputVisibility` |
19
+
20
+ Recipes:
21
+
22
+ - Run any of these offline: `lua test workflow <name>` (steering flags in `docs/api/Workflows.md` → "Testing locally").
23
+ - Give a predicate BOTH truth values under `--agents fake` with `--step-output <id>=<json>` — the fake stub alone
24
+ cannot reach the `gt(confidence, 0.6)` arm of `research-brief`.
25
+ - Every doc rule worth repeating: _execute re-runs from the top after resume; execution is at-least-once — dedupe
26
+ on `occurrenceId`_ (`${lineageId}:${stepId}`); an effect unique across independent runs needs your own business
27
+ key (`refund:${ticketId}`).
@@ -0,0 +1,48 @@
1
+ // Example (i) — adversarial verify: spawn finders until two consecutive rounds add nothing
2
+ // new, then have an independent verifier confirm each finding. Script form (04 §4.3);
3
+ // push with `lua push workflow`, run offline with `lua test workflow adversarial-verify`.
4
+ export const meta = {
5
+ name: 'adversarial-verify',
6
+ description: 'Find candidate issues from several angles, verify each independently, report the confirmed set',
7
+ phases: [{ title: 'Find' }, { title: 'Verify' }, { title: 'Report' }],
8
+ concurrency: 4,
9
+ sampleArgs: { subject: 'The attached design doc', angles: ['security', 'performance', 'clarity'] },
10
+ };
11
+
12
+ const found = new Map();
13
+ let quietRounds = 0;
14
+ for (let round = 1; round <= 5 && quietRounds < 2; round++) {
15
+ const batch = await parallel(
16
+ args.angles.map((angle) =>
17
+ agent(`Round ${round}: list concrete issues in ${args.subject} from the ${angle} angle. One per line.`, {
18
+ phase: 'Find',
19
+ label: `find-${angle}-${round}`,
20
+ })
21
+ )
22
+ );
23
+ // step(): a pure reduction journaled once — its closure may use anything, it never re-runs on replay.
24
+ const added = await step(`dedupe-${round}`, () => {
25
+ let fresh = 0;
26
+ for (const text of batch) {
27
+ for (const line of String(text).split('\n')) {
28
+ const key = line.trim().toLowerCase();
29
+ if (key && !found.has(key)) {
30
+ found.set(key, line.trim());
31
+ fresh++;
32
+ }
33
+ }
34
+ }
35
+ return fresh;
36
+ });
37
+ quietRounds = added === 0 ? quietRounds + 1 : 0;
38
+ log(`round ${round}: ${added} new finding(s)`);
39
+ }
40
+
41
+ const verdicts = await foreach(
42
+ [...found.values()],
43
+ (issue) =>
44
+ agent(`Independently verify: "${issue}". Answer CONFIRMED or REJECTED with one sentence.`, { phase: 'Verify' }),
45
+ { concurrency: 4 }
46
+ );
47
+ const confirmed = [...found.values()].filter((_, i) => String(verdicts[i]).toUpperCase().startsWith('CONFIRMED'));
48
+ return { confirmed, rejected: found.size - confirmed.length, rounds: quietRounds };
@@ -0,0 +1,19 @@
1
+ // Routes a PR review to the run named in the PR body (Workflows.signal).
2
+ // Verbatim from workflows-spec 03 §3.2 (f) (WF-215 / WF-223 — the spec is normative).
3
+ // src/webhooks/github-review.ts — routes a PR review to the run named in the PR body (§3.7 `Workflows.signal`; 06 §6.5.3 step 2b descends it into the waiting review-round child)
4
+ import { LuaWebhook, Workflows } from 'lua-cli';
5
+
6
+ export default new LuaWebhook({
7
+ name: 'github-pr-review',
8
+ description: 'GitHub pull_request_review → github.review signal',
9
+ async execute({ headers, body }) {
10
+ if (headers['x-github-event'] !== 'pull_request_review') return { ignored: true };
11
+ const runId = /<!-- lua-run:(wfr_[A-Za-z0-9_-]+) -->/.exec(body.pull_request?.body ?? '')?.[1]; // the marker `openPr` wrote (above); absent ⇒ not one of ours
12
+ if (!runId) return { ignored: true };
13
+ const state = body.review.state === 'approved' ? 'approved' : body.review.state === 'changes_requested' ? 'changes_requested' : 'commented';
14
+ const r = await Workflows.signal(runId, 'github.review',
15
+ { state, comments: [{ body: body.review.body ?? '' }] }, // matches the wait's `schema`; inline review comments arrive on `pull_request_review_comment` and are folded the same way
16
+ { dedupeKey: `review:${body.review.id}` }); // GitHub redelivers: one signal per review id (06 §6.5)
17
+ return { runId, accepted: r.accepted, reason: r.reason }; // `accepted:false, reason:'source_not_accepted'` would mean the wait's acceptedSources exclude 'webhook' — it does not (above)
18
+ },
19
+ });
@@ -0,0 +1,21 @@
1
+ // The Linear label trigger that starts ticket-to-pr (07 §7.2.1 TriggerStartWorkflow).
2
+ // Verbatim from workflows-spec 03 §3.2 (f) (WF-215 / WF-223 — the spec is normative).
3
+ // src/triggers/linear-ready.ts — the Linear label that starts a run (07 §7.2.1 `TriggerStartWorkflow`; LuaTrigger has no execute — filter + transform only)
4
+ import { LuaTrigger } from 'lua-cli';
5
+
6
+ export default new LuaTrigger({
7
+ name: 'linear-ready-for-agent',
8
+ description: 'Linear "issue labeled" webhook → start ticket-to-pr',
9
+ source: 'webhook',
10
+ filter: (ctx) => ctx.payload?.type === 'Issue' && ctx.payload?.action === 'update'
11
+ && (ctx.payload.data?.labels ?? []).some((l: { name: string }) => l.name === 'ready-for-agent'), // any other label edit: the trigger's ordinary "filtered" verdict, no run
12
+ transform: (ctx) => {
13
+ const issue = ctx.payload.data;
14
+ return { startWorkflow: {
15
+ name: 'ticket-to-pr',
16
+ input: { ticketId: issue.identifier, title: issue.title, spec: issue.description ?? '', repo: 'https://github.com/acme/backend', baseRef: 'main' },
17
+ idempotencyKey: `linear:${issue.identifier}:ready-for-agent`, // a redelivered webhook or a label toggled twice returns the SAME run (§3.7; 07 §7.2.3 step 2)
18
+ // no `notify`: a trigger start runs as the system principal, and `notify` addresses the creator only (07 §7.4.1 — inert here); the human-facing surface is the PR itself + the approval inbox
19
+ } };
20
+ },
21
+ });
@@ -0,0 +1,55 @@
1
+ // foreach + approval + an exactly-once send (`ctx.once`).
2
+ // Verbatim from workflows-spec 03 §3.2 (b) (WF-215 / WF-223 — the spec is normative).
3
+ import { z } from 'zod';
4
+ import { createStep, createWorkflow, fromInit, fromStep, template, AI, Channels } from 'lua-cli';
5
+
6
+ const lead = z.object({ email: z.string().email(), name: z.string() });
7
+ const draft = z.object({ to: z.string(), body: z.string() });
8
+
9
+ const draftEmail = createStep({
10
+ id: 'draftEmail',
11
+ inputSchema: lead, // foreach passes the RAW item — `{ email, name }`, not `{ lead: {…} }` (§3.2.1 check 4)
12
+ outputSchema: draft,
13
+ async execute({ inputData }) {
14
+ // string overload → Promise<string> (`api-exports.ts:768`); the object overload returns `AiGenerateOutput{ text, … }` (`:786`, shared-types `ai-generate.types.ts:78-86`)
15
+ const text = await AI.generate(`Write a 3-line intro email to ${inputData.name}`);
16
+ return { to: inputData.email, body: text };
17
+ },
18
+ });
19
+ const sendEmails = createStep({
20
+ id: 'sendEmails',
21
+ inputSchema: z.object({ drafts: z.array(draft) }),
22
+ outputSchema: z.object({ sent: z.number() }),
23
+ sideEffects: 'external', // never auto-retried on platform-fault reclaim
24
+ onError: 'park', // an EFFECT_IN_DOUBT parks the step for R37 instead of failing the run
25
+ async execute({ inputData, once }) {
26
+ let sent = 0;
27
+ for (const d of inputData.drafts) {
28
+ // exactly-once per {occurrenceId, key}: a retry, resume, repair run or migrated run that reaches this line again gets the stored
29
+ // result back and never re-sends (I27 claim/settle). No hand-rolled Data check-then-act — that pattern was non-atomic.
30
+ const r = await once(d.to, () => Channels.email.send({ to: { email: d.to }, subject: 'Hello', body: d.body })); // `EmailSendInput.to` is `{ userId?, email? }`
31
+ // The whole `Channels` facade is callable in-step — incl. `Channels.whatsapp.send({ threadId: ctx.runtime.replyTo!.threadId, text })` on a customer-channel run (07 §7.0-L / §7.4.5): a step MAY answer the customer
32
+ // itself (e.g. the refund outcome right after the Stripe step, before a QA step that must not delay it); `once()` covers it exactly like email, and the platform's terminal reply then lands as a
33
+ // duplicate-safe fallback (same threadId; the customer sees two lines only if the step's text differs). Outside WhatsApp's session the in-step send needs a template too — `Channels.whatsapp.sendTemplate` (closing pass 2026-08-27).
34
+ if (r) sent++;
35
+ }
36
+ return { sent };
37
+ },
38
+ });
39
+
40
+ export const outreach = createWorkflow({
41
+ name: 'outreach',
42
+ inputSchema: z.object({ leads: z.array(lead) }),
43
+ outputSchema: z.object({ sent: z.number() }),
44
+ schedule: { type: 'cron', expression: '0 9 * * 1', timezone: 'Europe/London' },
45
+ scheduleInput: { leads: [] },
46
+ concurrencyPolicy: 'forbid',
47
+ })
48
+ .map({ leads: fromInit('leads') }, { id: 'leads' }) // entry 1 — three maps in this workflow ⇒ every id explicit (`map-id-required`)
49
+ .map({ '': fromStep('leads', 'leads') }, { id: 'items' }) // entry 2 — '' key = "output IS this value" (Lua extension, §3.4): foreach needs a raw array upstream
50
+ .foreach(draftEmail, { concurrency: 8, maxItems: 500 }) // entry 3 — items are `lead`s, output is `draft[]`
51
+ .map({ drafts: fromStep('draftEmail') }, { id: 'drafts' }) // entry 4 — `{ drafts: draft[] }`, the approval's editable payload
52
+ .approval('reviewDrafts', { title: 'Approve outreach batch', details: template('${stepResults.drafts.drafts.length} drafts ready'),
53
+ approver: 'org-admins', timeoutHours: 48, onTimeout: 'cancel-run', editable: true, editablePaths: ['drafts', 'drafts[*].body'] }) // entry 5
54
+ .then(sendEmails) // entry 6 — reads `stepResults.drafts` (the approval passes its edited payload through)
55
+ .commit();
@@ -0,0 +1,75 @@
1
+ // One review iteration; runs as a child of ticket-to-pr with the SAME workspace.
2
+ // From workflows-spec 03 §3.2 (f) (WF-215 / WF-223 — the spec is normative). LUA-635: a switch arm is ONE step and a
3
+ // string arm names an agentStep / specialistStep / toolStep / map / workflow declaration (03 §3.2.0) — the listing's
4
+ // `otherwise: 'done'` over a later `.then(createStep({ id: 'done' }))` was a goto the placement rule never had.
5
+ // src/workflows/pr-review-round.ts — one review iteration; runs as a child of ticket-to-pr with the SAME workspace
6
+ import { z } from 'zod';
7
+ import { createStep, createWorkflow, step, eq, lit, template } from 'lua-cli';
8
+
9
+ const pushFix = createStep({
10
+ id: 'pushFix',
11
+ inputSchema: z.any(),
12
+ outputSchema: z.object({ headSha: z.string() }),
13
+ tier: 'job',
14
+ workspace: { mount: 'rw' },
15
+ timeoutSeconds: 600,
16
+ async execute({ workspace }) {
17
+ // git runs through the credential proxy — no token in this container (05 §5.17.3)
18
+ const sha = (await $`git -C ${workspace!.path} rev-parse HEAD`).stdout.trim(); // `$` = the Job image's shell helper (`@lua/coding-harness/shell`); a plain child_process spawn works too — the Job site has no REQUIRE_BLOCKLIST for `child_process`
19
+ return { headSha: sha };
20
+ },
21
+ });
22
+
23
+ const changesRequested = eq(step('review').path('payload.state'), lit('changes_requested'));
24
+
25
+ export const prReviewRound = createWorkflow({
26
+ name: 'pr-review-round',
27
+ inputSchema: z.object({ prNumber: z.number(), repo: z.string() }),
28
+ outputSchema: z.object({
29
+ state: z.enum(['approved', 'changes_requested', 'timed_out']),
30
+ round: z.number().optional(),
31
+ }),
32
+ })
33
+ .waitForSignal('review', {
34
+ signal: 'github.review',
35
+ timeoutHours: 168, // delivered by the GitHub webhook (§3.7) — to the PARENT run; the engine descends it into this child (06 §6.5.3 step 2b)
36
+ schema: z.object({
37
+ state: z.enum(['approved', 'changes_requested', 'commented']),
38
+ comments: z.array(z.object({ path: z.string().optional(), body: z.string() })),
39
+ }),
40
+ acceptedSources: ['webhook'],
41
+ onTimeout: 'continue',
42
+ })
43
+ .switch([[changesRequested, 'addressReview']]) // no `otherwise`: approved / commented / timed out falls through to `done`
44
+ .agentStep('addressReview', {
45
+ // a CODING TURN: Claude Code headless with git/gh/shell/edit on the mounted checkout (05 §5.17.6)
46
+ agentId: 'swe-implementer',
47
+ tier: 'job',
48
+ workspace: { mount: 'rw' },
49
+ timeoutSeconds: 7200,
50
+ jobResources: 'medium',
51
+ prompt: template(
52
+ 'Address every review comment in ${stepResults.review.payload.comments} on the current branch. Run the relevant tests. Commit with a message that references the comment you addressed. Do not force-push.'
53
+ ),
54
+ toolScope: { jobTools: ['shell', 'read', 'write', 'edit', 'glob', 'grep', 'git'] }, // no `gh`: the push + PR update is the next code step
55
+ outputSchema: z.object({ summary: z.string() }),
56
+ })
57
+ .switch([[changesRequested, pushFix]]) // the fix path's second step — same predicate, an inline createStep as the arm
58
+ .then(
59
+ createStep({
60
+ id: 'done',
61
+ inputSchema: z.any(),
62
+ outputSchema: z.object({ state: z.enum(['approved', 'changes_requested', 'timed_out']) }),
63
+ async execute({ getStepResult }) {
64
+ const r = getStepResult<{ received: boolean; timedOut?: boolean; payload?: { state: string } }>('review');
65
+ return {
66
+ state: r?.received
67
+ ? r.payload!.state === 'changes_requested'
68
+ ? 'changes_requested'
69
+ : 'approved'
70
+ : 'timed_out',
71
+ };
72
+ },
73
+ })
74
+ )
75
+ .commit();
@@ -0,0 +1,35 @@
1
+ // Nested workflow + sleep + signal.
2
+ // From workflows-spec 03 §3.2 (c) (WF-215 / WF-223 — the spec is normative). LUA-635: the switch arms are the
3
+ // createStep objects themselves — a STRING arm names an agentStep / specialistStep / toolStep / map / workflow
4
+ // declaration elsewhere in the chain (03 §3.2.0); `.then(createStep({ id }))` places a step, it never declares one.
5
+ import { z } from 'zod';
6
+ import { createStep, createWorkflow, step, eq, lit, fromInit } from 'lua-cli';
7
+
8
+ const finalize = createStep({
9
+ id: 'finalize',
10
+ inputSchema: z.any(),
11
+ outputSchema: z.object({ ready: z.boolean() }),
12
+ execute: async () => ({ ready: true }),
13
+ });
14
+ const rollback = createStep({
15
+ id: 'rollback',
16
+ inputSchema: z.any(),
17
+ outputSchema: z.object({ ready: z.boolean() }),
18
+ execute: async () => ({ ready: false }),
19
+ });
20
+
21
+ export const provision = createWorkflow({
22
+ name: 'provision-tenant',
23
+ inputSchema: z.object({ tenantId: z.string() }),
24
+ outputSchema: z.object({ ready: z.boolean() }),
25
+ })
26
+ .workflow('createResources', 'research-brief', { topic: fromInit('tenantId') }) // nested by name (same agent) or LuaWorkflow ref
27
+ .sleep(5 * 60 * 1000) // engine-side; holds no compute
28
+ .waitForSignal('vendorReady', {
29
+ signal: 'vendor.ready',
30
+ schema: z.object({ ok: z.boolean() }),
31
+ timeoutHours: 72,
32
+ acceptedSources: ['webhook', 'api'],
33
+ })
34
+ .switch([[eq(step('vendorReady').path('ok'), lit(true)), finalize]], rollback) // inline code steps; the last entry is a conditional, so the taken arm's output is the run output
35
+ .commit();
@@ -0,0 +1,57 @@
1
+ // Maker-checker refund with escalation, business-hours deadlines and a recoverable external step.
2
+ // Verbatim from workflows-spec 03 §3.2 (e) (WF-215 / WF-223 — the spec is normative).
3
+ import { z } from 'zod';
4
+ import { createStep, createWorkflow, template, Integrations } from 'lua-cli';
5
+
6
+ const postRefund = createStep({
7
+ id: 'postRefund',
8
+ inputSchema: z.object({
9
+ approved: z.boolean(),
10
+ editedPayload: z.object({ ticketId: z.string(), amount: z.number() }).optional(),
11
+ input: z.object({ ticketId: z.string(), amount: z.number() }),
12
+ }), // the approval node's output shape (04 §4.2.3)
13
+ outputSchema: z.object({ refundId: z.string().nullable() }),
14
+ sideEffects: 'external', // a platform-fault reclaim PARKS this step (never auto-retried); the run gates `exception` (§06 §6.3.5)
15
+ onError: 'park', // a final customer-fault failure parks too — the operator retries / skips / supplies the refundId / fails it
16
+ requiredConnections: ['stripe'], // the declared key below — resolved on the owner agent at run time (LUA-623); no connection of that type ⇒ failed{credentials_unresolved}, unmountable ⇒ failed{credentials_revoked, reason:'required_connection_unmountable'} → parked via onError:'park' (§11 §11.5.5)
17
+ async execute({ inputData, occurrenceId }) {
18
+ if (!inputData.approved) return { refundId: null };
19
+ const r = inputData.editedPayload ?? inputData.input;
20
+ // `occurrenceId` is `${lineageId}:${stepId}`: the same key on retry-step, on resume and on a REPAIR RUN — the refund collapses to one (§06 §6.3.3 (e)).
21
+ // A key that must hold across INDEPENDENT runs (two runs for the same ticket) is the caller's: `refund:${r.ticketId}` (§06 §6.3.3 (f)).
22
+ const res = await Integrations.passthrough('stripe', {
23
+ method: 'POST',
24
+ path: '/v1/refunds',
25
+ headers: { 'Idempotency-Key': `refund:${r.ticketId}` },
26
+ body: { charge: r.ticketId, amount: r.amount },
27
+ });
28
+ return { refundId: res.body.id };
29
+ },
30
+ });
31
+
32
+ export const refund = createWorkflow({
33
+ name: 'refund-approval',
34
+ inputSchema: z.object({ ticketId: z.string(), amount: z.number(), requesterId: z.string() }),
35
+ outputSchema: z.object({ refundId: z.string().nullable() }),
36
+ budget: { maxDurationSeconds: 14 * 24 * 3600 }, // long enough for two business-hours escalation hops (the compiler warns `deadline-clamped` otherwise)
37
+ connections: [{ key: 'stripe', integrationType: 'stripe', required: true }], // the Stripe connection the refund step acts through, declared once by key — never a frozen connection id (LUA-623)
38
+ })
39
+ .approval('approveRefund', {
40
+ title: 'Refund request',
41
+ details: template('Refund ${initData.amount} for ticket ${initData.ticketId}'),
42
+ approver: { role: 'support-lead' }, // org role, template-portable; `{users:[…]}` / `{group:'finance'}` are org data (§13 §13.3.3)
43
+ excludeInitiator: true, // the requester who started the run can never approve it (maker-checker)
44
+ fourEyes: { edit: { role: 'support-lead' }, approve: { role: 'finance-controller' } }, // whoever edits the amount cannot be the one who approves it
45
+ timeoutHours: 8,
46
+ businessHours: { tz: 'Europe/London', calendar: 'mon-fri' }, // 8 business hours, then…
47
+ onTimeout: [
48
+ { escalateTo: { role: 'finance-controller' }, timeoutHours: 16 }, // …hop 1: finance, 16 business hours, then…
49
+ { escalateTo: 'org-admins', timeoutHours: 24 }, // …hop 2: org admins, then…
50
+ 'deny',
51
+ ], // …the node completes {approved:false, timedOut:true, escalations:2} — branchable data (§06 §6.4.11)
52
+ editable: true,
53
+ editablePaths: ['amount'], // the approver may lower the amount; the approve call echoes the fingerprint of the revision it saw (§06 §6.4.9)
54
+ editedPayloadSchema: z.object({ ticketId: z.string(), amount: z.number().positive().max(500) }),
55
+ })
56
+ .then(postRefund)
57
+ .commit();
@@ -0,0 +1,42 @@
1
+ // Sequential + parallel research, typed end to end.
2
+ // Verbatim from workflows-spec 03 §3.2 (a) (WF-215 / WF-223 — the spec is normative).
3
+ import { z } from 'zod';
4
+ import { createStep, createWorkflow, stepOf, fromStep, template, gt, lit } from 'lua-cli';
5
+
6
+ const fetchSources = createStep({
7
+ id: 'fetchSources',
8
+ inputSchema: z.object({ topic: z.string() }),
9
+ outputSchema: z.object({ urls: z.array(z.string().url()) }),
10
+ timeoutSeconds: 60,
11
+ async execute({ inputData, log }) {
12
+ const res = await fetch(`https://api.example.com/search?q=${encodeURIComponent(inputData.topic)}`);
13
+ if (!res.ok) throw new Error(`search failed: ${res.status}`);
14
+ const json = (await res.json()) as { url: string }[];
15
+ log(`found ${json.length} sources`);
16
+ return { urls: json.slice(0, 10).map((r) => r.url) };
17
+ },
18
+ });
19
+
20
+ const angle = z.object({ summary: z.string(), confidence: z.number() });
21
+
22
+ const wf = createWorkflow({
23
+ name: 'research-brief',
24
+ description: 'Fetch sources, summarise from two angles in parallel, merge — or fall back when confidence is low.',
25
+ inputSchema: z.object({ topic: z.string() }),
26
+ outputSchema: z.object({ brief: z.string() }),
27
+ budget: { maxCredits: 40 },
28
+ })
29
+ .then(fetchSources) // entry 1: step
30
+ .parallel(['techAngle', 'marketAngle']) // entry 2: parallel — 2 arms, both declared BELOW (§3.2.0: "declare here, inside me")
31
+ .agentStep('techAngle', { agentId: 'analyst', prompt: template('Summarise the technical angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
32
+ outputSchema: angle }) // no top-level entry — placed by the parallel above
33
+ .agentStep('marketAngle', { agentId: 'analyst', prompt: template('Summarise the market angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
34
+ outputSchema: angle }) // same
35
+ .switch( // entry 3: conditional{exclusive}
36
+ [[gt(stepOf<typeof angle>('techAngle').path('confidence'), lit(0.6)), 'merge']], // TypedRef<number> vs Literal<number> — `lit('a')` would not compile
37
+ 'lowConfidence')
38
+ .map({ brief: fromStep('techAngle', 'summary'), market: fromStep('marketAngle', 'summary') }, { id: 'merge' }) // placed INSIDE the switch arm (string ref above) — one entry, not two
39
+ .agentStep('lowConfidence', { agentId: 'analyst', // placed as the switch's `otherwise`
40
+ prompt: template('Confidence was low. Write a cautious brief on ${initData.topic} from ${stepResults.techAngle.summary} and ${stepResults.marketAngle.summary}'),
41
+ outputSchema: z.object({ brief: z.string() }) })
42
+ .commit(); // the conditional is the last entry: the taken arm's output IS the run output
@@ -0,0 +1,19 @@
1
+ // Ephemeral reviewer on the owning agent (D25).
2
+ // Verbatim from workflows-spec 03 §3.2 (d) (WF-215 / WF-223 — the spec is normative).
3
+ import { z } from 'zod';
4
+ import { createWorkflow, template } from 'lua-cli';
5
+
6
+ export const reviewedBrief = createWorkflow({
7
+ name: 'reviewed-brief',
8
+ inputSchema: z.object({ company: z.string() }),
9
+ outputSchema: z.object({ findings: z.array(z.string()), verdict: z.enum(['pass', 'revise']) }),
10
+ })
11
+ .agentStep('draft', { agentId: 'web-researcher', prompt: template('Draft a one-page brief on ${initData.company}.') })
12
+ .specialistStep('review', {
13
+ role: { name: 'Reviewer',
14
+ instructions: 'You are a sceptical reviewer. Check every claim in the draft against a source you can cite; flag anything unsupported. Never rewrite the draft — return findings only.',
15
+ tools: ['searchWeb', 'fetchUrl'] }, // ⊆ the owning agent's toolset; delegation tools (`agent-*`) are never allowlistable (04 §4.2.7)
16
+ prompt: template('Review the draft: ${stepResults.draft.text}'),
17
+ outputSchema: z.object({ findings: z.array(z.string()), verdict: z.enum(['pass', 'revise']) }),
18
+ })
19
+ .commit();
@@ -0,0 +1,81 @@
1
+ // Support triage from a customer channel — knowledge grounding, toolScope, dataset ref, ctx.artefacts.
2
+ // From workflows-spec 03 §3.2 (g) (WF-215 / WF-223 — the spec is normative). LUA-635: the listing's `.conditional([{ when, branch }])`
3
+ // is not in the builder (03 §3.1 has `switch` / `branch` over `[predicate, StepRef]` arms) and it named an approval as an arm, which
4
+ // v1 forbids (approvals are top-level only) — the gate is a `switch` whose confident arm is (e)'s `refund-approval` as a child run.
5
+ // (The listing's `template(…)` approval title was the other drift: the grammar is `title: string`, bindings ride `details`.)
6
+ import {
7
+ createWorkflow,
8
+ createStep,
9
+ fromInit,
10
+ fromStep,
11
+ fromKnowledge,
12
+ rows,
13
+ template,
14
+ stepOf,
15
+ gt,
16
+ lit,
17
+ } from 'lua-cli';
18
+ import { z } from 'zod';
19
+
20
+ const pullHistory = createStep({
21
+ // array-typed output ⇒ oversize (> 8 MB) becomes a `{__datasetRef}` (NDJSON on CDN), never OUTPUT_TOO_LARGE
22
+ id: 'pullHistory',
23
+ inputSchema: z.object({ customerId: z.string() }),
24
+ outputSchema: z.object({ orders: z.array(z.object({ id: z.string(), total: z.number(), status: z.string() })) }),
25
+ execute: async (ctx) => {
26
+ const orders = await Orders.list({ customerId: ctx.inputData.customerId, limit: 50_000 });
27
+ const csv = ['id,total,status', ...orders.map((o) => `${o.id},${o.total},${o.status}`)].join('\n');
28
+ const { artefactId } = await ctx.artefacts.put('orders.csv', csv, {
29
+ // P1-8: journaled in script form; ≤ 50 per step
30
+ contentType: 'text/csv',
31
+ kind: 'dataset',
32
+ datasetSchema: {
33
+ type: 'object',
34
+ properties: { id: { type: 'string' }, total: { type: 'number' }, status: { type: 'string' } },
35
+ },
36
+ });
37
+ ctx.log(`orders.csv → ${artefactId}`);
38
+ return { orders };
39
+ },
40
+ });
41
+
42
+ const classifyOut = z.object({
43
+ intent: z.enum(['refund', 'status', 'other']),
44
+ confidence: z.number(),
45
+ orderId: z.string().optional(),
46
+ amount: z.number().optional(),
47
+ }); // `amount`: the order total a confident refund carries into (e)
48
+
49
+ export default createWorkflow({
50
+ name: 'support-triage',
51
+ inputSchema: z.object({ ticketId: z.string(), customerId: z.string(), message: z.string() }),
52
+ })
53
+ .then(pullHistory)
54
+ .map(
55
+ {
56
+ refund: fromKnowledge({ source: 'org-docs', query: 'refund policy', maxChars: 4000, topK: 3 }), // rendered at S2 with a provenance header per chunk
57
+ recent: rows('pullHistory', 'orders', { offset: 0, limit: 20 }), // paged rows; the bare ref would be the {__datasetRef} object
58
+ },
59
+ { id: 'classifyInputs' }
60
+ )
61
+ .agentStep('classify', {
62
+ agentId: 'support-agent',
63
+ // `${initData.message}` is customer-channel content and `refund` is a {knowledge} binding: this step is EXTERNAL-CONTENT — `toolScope` is mandatory.
64
+ prompt: template(
65
+ 'Ticket ${initData.ticketId}: ${initData.message}\n\nRefund policy:\n${stepResults.classifyInputs.refund}\n\nRecent orders (first page):\n${stepResults.classifyInputs.recent}'
66
+ ),
67
+ toolScope: {}, // `{}` = no tools; `defaultToolScopeMode:'deny'` would imply this when absent
68
+ outputSchema: classifyOut,
69
+ })
70
+ .switch([[gt(stepOf<typeof classifyOut>('classify').path('confidence'), lit(0.8)), 'refundGate']], 'handoff') // a confident refund goes to the gate, anything else to a human. An approval is TOP-LEVEL only (03 §3.1), so the gated one is a child run whose approval sits at its own top level — the same shape as a wait inside a loop (03 §3.2 (f))
71
+ .workflow('refundGate', 'refund-approval', {
72
+ ticketId: fromInit('ticketId'),
73
+ amount: fromStep('classify', 'amount'),
74
+ requesterId: fromInit('customerId'),
75
+ }) // (e)'s maker-checker run, nested by name on the same agent; declared here, placed inside the switch arm by the string ref above. Its approver is a role, NOT 'creator': `approver-is-run-principal` on a customer-channel workflow
76
+ .agentStep('handoff', {
77
+ agentId: 'support-agent',
78
+ prompt: template('Summarise ${initData.message} for a human agent.'),
79
+ toolScope: {},
80
+ }) // the switch's `otherwise`
81
+ .commit();