lua-cli 3.46.0 → 3.48.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.
@@ -258,7 +258,7 @@ lua push [type] [options]
258
258
 
259
259
  **Arguments:**
260
260
 
261
- - `type` - Primitive type to push: `skill`, `webhook`, `job`, `preprocessor`, `postprocessor`, `mcp`, `device`, `all`
261
+ - `type` - Primitive type to push: `skill`, `agent`, `webhook`, `trigger`, `job`, `preprocessor`, `postprocessor`, `mcp`, `device`, `device-trigger`, `voice`, `decision`, `workflow`, `webapp`, `backup`, `all`
262
262
 
263
263
  **Options:**
264
264
 
@@ -587,6 +587,83 @@ $ lua jobs run daily-cleanup
587
587
 
588
588
  ---
589
589
 
590
+ ### `lua decisions`
591
+
592
+ See, try and roll back the named decisions (`LuaDecision`) on the agent.
593
+
594
+ **Usage:**
595
+
596
+ ```bash
597
+ lua decisions [command] [name] [version] [options]
598
+ ```
599
+
600
+ **Commands:**
601
+
602
+ - `list` - List the decisions with their current version and source (default)
603
+ - `versions <name>` - List a decision's versions, newest first; the current one is marked
604
+ - `publish <name> <version>` - Make that version current (rollback); asks first unless `--force` or `--ci`
605
+ - `run <name>` - Run the current version on `--state '<json>'` or `--message '<text>'`
606
+ - `delete <name>` - Deactivate the decision and keep its versions; asks first, and needs `--force` in CI
607
+
608
+ **Aliases:**
609
+
610
+ - `ls`, `view` - `list`
611
+ - `history`, `list-versions` - `versions`
612
+ - `rollback`, `deploy` - `publish`
613
+ - `try`, `test` - `run`
614
+ - `rm`, `remove`, `del` - `delete`
615
+
616
+ **Options:**
617
+
618
+ - `--json` - Machine-readable output (`list`, `versions`, `run`)
619
+ - `--force` - Skip the confirmation (`publish`, `delete`)
620
+ - `--state <json>` - `run`: the state to decide on
621
+ - `--message <text>` - `run`: sent as `{ message: <text> }`; an empty or blank message is a usage error (exit 2)
622
+ - `--provider <jev|openai|auto>` - `run`: override the version's provider
623
+ - `--min-confidence <n>` - `run`: override the version's `minConfidence` (0 to 1)
624
+
625
+ **Example:**
626
+
627
+ ```bash
628
+ $ lua decisions
629
+ 🧭 triage
630
+ Routes inbound support messages
631
+ Current version: 1.0.1
632
+ Source: from code
633
+ Created: 10/1/2026, 10:00:00 AM
634
+
635
+ $ lua decisions run triage --message 'I want my money back'
636
+ refund: odds 0.93
637
+ team: billing (confidence 0.62, uncertain)
638
+
639
+ Provider: jev
640
+ Rule: default
641
+ Model: jev-latest
642
+ Duration: 184ms
643
+ Tokens: 412
644
+ Fallback: none
645
+
646
+ $ lua decisions publish triage 1.0.0 --force
647
+ ✅ "triage": 1.0.1 → 1.0.0
648
+ Questions removed: team
649
+ ```
650
+
651
+ The next `lua version promote` sets each decision to the version that agent version included.
652
+
653
+ `publish` on a deleted decision turns it back on. If another decision has taken its name since, the server answers 409 and the command exits 10.
654
+
655
+ A failed `run` prints the decision error's own message, so a timeout or a provider outage (both 503) reads as one and exits 11. `lua logs --type decisions` shows the failed call's log row.
656
+
657
+ **Exit codes:**
658
+
659
+ - `2` - An unknown decision name or version, a missing argument, or `delete` in CI without `--force`
660
+ - `3` - The server answered 404
661
+ - `9` - The server refused the credential (401)
662
+ - `10` - The server answered 402, 403, 422 or another 4xx
663
+ - `11` - The server answered 503, 504 or another 5xx, or could not be reached
664
+
665
+ ---
666
+
590
667
  ### `lua integrations`
591
668
 
592
669
  Connect third-party accounts and services.
package/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # lua-cli v3.46.0
1
+ # lua-cli v3.47.0
2
2
 
3
- Welcome to the comprehensive API documentation for lua-cli v3.43.0. This guide covers every API, class, and function exported by the package.
3
+ Welcome to the comprehensive API documentation for lua-cli v3.47.0. This guide covers every API, class, and function exported by the package.
4
4
 
5
5
  ## 📚 Documentation Index
6
6
 
@@ -263,8 +263,8 @@ Each API doc includes:
263
263
 
264
264
  ---
265
265
 
266
- **Version:** 3.29.1
267
- **Last Updated:** August 2026
266
+ **Version:** 3.47.0
267
+ **Last Updated:** October 2026
268
268
 
269
269
  ---
270
270
 
@@ -0,0 +1,276 @@
1
+ # Decisions API
2
+
3
+ The Decisions API asks a fast classifier model short, structured questions about any state and returns typed answers you can branch on.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import { Decisions, DecisionError } from 'lua-cli';
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ Use Decisions when your code needs a judgment, not prose.
14
+
15
+ - Ask a yes or no question and get the odds that the answer is yes.
16
+ - Pick one option from a fixed set, such as which team should handle a message.
17
+ - Place something on an ordered scale, such as how frustrated a customer sounds.
18
+ - Ask many questions about the same state in one call.
19
+
20
+ Each call is billed a flat amount: 0.005 actions when Jev answers and 0.01 actions when OpenAI answers. A cached hit and a failed call cost nothing. A call never goes through the agent chat pipeline.
21
+
22
+ ## Methods
23
+
24
+ ### `Decisions.ask(request)`
25
+
26
+ Asks every question in `request.questions` about `request.state` and resolves the full result.
27
+
28
+ ```typescript
29
+ const result = await Decisions.ask({
30
+ state: { message: input.text },
31
+ questions: {
32
+ refund: Decisions.odds('Is the customer asking for a refund?'),
33
+ team: Decisions.choice('Which team should handle this?', {
34
+ billing: 'Payments, invoices, refunds and charges.',
35
+ tech: 'Bugs, errors and anything that does not work.',
36
+ sales: 'Pricing, plans and buying more.',
37
+ }),
38
+ frustration: Decisions.score('How frustrated is the customer?', ['calm', 'annoyed', 'angry']),
39
+ },
40
+ minConfidence: 0.7,
41
+ });
42
+
43
+ const { refund, team, frustration } = result.answers;
44
+ if (refund.type === 'odds' && refund.odds > 0.8) {
45
+ return { route: 'refunds' };
46
+ }
47
+ if (team.type === 'choice' && !team.uncertain) {
48
+ return { route: team.choice, escalate: frustration.type === 'score' && frustration.level === 'angry' };
49
+ }
50
+ ```
51
+
52
+ `state` can be a string, an array of strings, or a plain object. An empty state (`""`, a blank string, `[]`, `{}`, or an object whose values are all blank strings, such as `{ message: '' }`) throws `decision_invalid`, and the call is never sent or billed. So does a state or question instructions holding a lone UTF-16 surrogate (half of an emoji cut in two), which no provider accepts.
53
+
54
+ You can pass up to 4 `images` (`{ data, mimeType }`, base64 data) next to the state, each at most 5 MB and at most 6 MB in total. A call over the total throws `decision_invalid` with a message that names the total. A call with images always goes to OpenAI, because Jev does not take images.
55
+
56
+ A request body over 10 MB is refused with HTTP 413 before any of these checks run. Base64 makes images about a third larger, so the 6 MB image total is the practical limit for one call.
57
+
58
+ Jev takes about 32,000 tokens of input. With `provider: 'auto'`, a larger state falls back to OpenAI and is charged at OpenAI's price. With `provider: 'jev'`, it fails with `decision_rejected` and Jev's own message.
59
+
60
+ ### `Decisions.ask(state, question, options?)`
61
+
62
+ Asks a single question and resolves just its answer, not a map.
63
+
64
+ ```typescript
65
+ const refund = await Decisions.ask(input.text, Decisions.odds('Is the customer asking for a refund?'));
66
+ if (refund.type === 'odds' && refund.odds > 0.8) {
67
+ return { route: 'refunds' };
68
+ }
69
+ ```
70
+
71
+ `options` takes the same fields as the options table below.
72
+
73
+ ### `Decisions.run(name, state, options?)`
74
+
75
+ Runs a decision declared on the agent with [`LuaDecision`](./LuaDecision.md) and resolves the same result as `Decisions.ask`.
76
+
77
+ ```typescript
78
+ const result = await Decisions.run('triage', { message: input.text });
79
+ const strict = await Decisions.run('triage', { message: input.text }, { minConfidence: 0.9 });
80
+ ```
81
+
82
+ The platform asks the questions of the decision's current version, with its options. Any field in `options` (the options table below, plus `images`) overrides the version's value for this call only.
83
+
84
+ Each lua-core pod caches the current version. A promote, a publish, a rename or a delete clears that cache at once, so a new version or a rollback reaches running code without a redeploy.
85
+
86
+ A name the agent does not declare throws a `DecisionError` with code `decision_not_found`. So does a decision that was pushed but never made current.
87
+
88
+ ### `Decisions.odds(instructions, criteria?)`
89
+
90
+ Builds a yes or no question whose answer is the probability that the answer is yes.
91
+
92
+ ```typescript
93
+ Decisions.odds('Is this message spam?', {
94
+ yes: 'Unsolicited offers, links to unknown sites, or requests for credentials.',
95
+ no: 'A real question or request from a customer.',
96
+ });
97
+ ```
98
+
99
+ ### `Decisions.choice(instructions, options)`
100
+
101
+ Builds a question whose answer is exactly one of the option keys, each described in plain words.
102
+
103
+ The option keys stay literal types, so `answer.choice` is typed as `'billing' | 'tech' | 'sales'` in the example above, once you have checked that `answer.type` is `'choice'`.
104
+
105
+ ### `Decisions.score(instructions, levels)`
106
+
107
+ Builds a question that places the state on an ordered scale of 2 to 10 levels.
108
+
109
+ A level is a label, or `{ label, description }` when the label needs explaining.
110
+
111
+ ### Checks before the call
112
+
113
+ The builders check the question before any network call. They throw `decision_invalid` for empty instructions, instructions over 4,000 characters, fewer than 2 or more than 64 options, fewer than 2 or more than 10 levels, or `criteria` that is not an object.
114
+
115
+ Question names and option labels are at most 64 characters. Any name works, including `constructor` or `toString`.
116
+
117
+ ## Response shape
118
+
119
+ ```typescript
120
+ interface DecisionResult {
121
+ decisionId: string;
122
+ answers: { [questionName: string]: DecisionAnswer };
123
+ provider: 'jev' | 'openai';
124
+ model: string;
125
+ rule: 'pinned' | 'images' | 'region' | 'zero_retention' | 'default' | 'fallback';
126
+ usage: { inputTokens: number | null };
127
+ durationMs: number;
128
+ cached: boolean;
129
+ fallback?: { from: 'jev' | 'openai'; reason: string };
130
+ }
131
+ ```
132
+
133
+ Each answer has a `type` you check before reading it. Any answer can be `refused`, so `.odds`, `.choice` and `.score` exist only after that check.
134
+
135
+ | `type` | Fields | Meaning |
136
+ | --------- | ------------------------------------------------------------ | --------------------------------------------------------- |
137
+ | `odds` | `odds` | The probability, from 0 to 1, that the answer is yes. |
138
+ | `choice` | `choice`, `confidence`, `probabilities`, `uncertain` | The chosen option key and how sure the model is about it. |
139
+ | `score` | `score`, `level`, `confidence`, `probabilities`, `uncertain` | The position on the scale and the matching level label. |
140
+ | `refused` | none | OpenAI declined to answer this question; Jev never does. |
141
+
142
+ `uncertain` is `true` when `confidence` is below `minConfidence`.
143
+
144
+ A cached hit reports `usage.inputTokens: 0`. Identical cacheable calls made at the same time share one provider call, and every call that joined it comes back with `cached: true`. That sharing happens on each server, so identical calls that reach two servers at once can make two provider calls. Calls that differ in `region` or `zeroRetention` never share an answer.
145
+
146
+ ## Examples
147
+
148
+ ### Pre-processor that routes a message
149
+
150
+ ```typescript
151
+ import { PreProcessor, Decisions } from 'lua-cli';
152
+
153
+ export const router = new PreProcessor({
154
+ name: 'support-router',
155
+ description: 'Routes every message to the team that should handle it.',
156
+ execute: async (user, messages) => {
157
+ const text = messages.flatMap((m) => (m.type === 'text' ? [m.text] : [])).join('\n');
158
+ const { answers } = await Decisions.ask({
159
+ state: text,
160
+ questions: {
161
+ team: Decisions.choice('Which team should handle this message?', {
162
+ billing: 'Payments, invoices, refunds and charges.',
163
+ tech: 'Bugs, errors and anything that does not work.',
164
+ sales: 'Pricing, plans and buying more.',
165
+ }),
166
+ },
167
+ });
168
+ const team = answers.team.type === 'choice' && !answers.team.uncertain ? answers.team.choice : 'general';
169
+ return { action: 'proceed', metadata: { team } };
170
+ },
171
+ });
172
+ ```
173
+
174
+ ### Webhook that gates on odds
175
+
176
+ ```typescript
177
+ import { LuaWebhook, Decisions, Data } from 'lua-cli';
178
+
179
+ export const inbound = new LuaWebhook({
180
+ name: 'inbound-email',
181
+ description: 'Saves inbound emails that ask for a refund.',
182
+ execute: async (event) => {
183
+ const refund = await Decisions.ask(
184
+ { subject: event.body.subject, body: event.body.text },
185
+ Decisions.odds('Is the sender asking for a refund?'),
186
+ { timeoutMs: 5000 }
187
+ );
188
+ if (refund.type !== 'odds' || refund.odds < 0.8) {
189
+ return { saved: false };
190
+ }
191
+ await Data.create('refund-requests', { subject: event.body.subject, odds: refund.odds });
192
+ return { saved: true };
193
+ },
194
+ });
195
+ ```
196
+
197
+ ## Error handling
198
+
199
+ A failed call throws a `DecisionError` with the server's message and these fields.
200
+
201
+ | Field | Description |
202
+ | ---------------- | ---------------------------------------------------------------------- |
203
+ | `code` | One of the codes in the table below. |
204
+ | `provider` | The provider that failed, when the failure came from a provider. |
205
+ | `upstreamStatus` | The HTTP status that provider answered, when it answered with one. |
206
+ | `decisionId` | The id of the call's log row, so `lua logs --type decisions` finds it. |
207
+
208
+ ```typescript
209
+ try {
210
+ await Decisions.ask(input.text, Decisions.odds('Is this urgent?'));
211
+ } catch (error) {
212
+ if (error instanceof DecisionError && error.code === 'decision_timeout') {
213
+ return { urgent: false };
214
+ }
215
+ throw error;
216
+ }
217
+ ```
218
+
219
+ | `code` | When it happens |
220
+ | ------------------------------- | ---------------------------------------------------------------------------------- |
221
+ | `decision_invalid` | The request breaks a rule, such as too many questions or an empty state. |
222
+ | `decision_timeout` | The provider did not answer before `timeoutMs`. |
223
+ | `decision_rejected` | The provider refused the request, such as a model it does not know. |
224
+ | `decision_unavailable` | No provider could answer, or this runtime cannot make decisions. |
225
+ | `decision_disabled` | The provider has no API key configured on the server. |
226
+ | `decision_insufficient_credits` | The organization has no credits left to pay for the call. |
227
+ | `decision_not_found` | `Decisions.run` named a decision the agent does not declare or never made current. |
228
+
229
+ The SDK sends a decision call once and never retries it. The server tries the other provider once when the first one times out or is unavailable, which includes a provider answering 401, 402, 403 or 429. It does not when you pin `provider`, unless you also set `fallback`.
230
+
231
+ `timeoutMs` is the budget for the whole call. When the first provider fails and a fallback runs, the fallback gets the time that is left. With under 100 ms left, there is no fallback. A short timeout on your call does not affect other calls.
232
+
233
+ A wrong `model` for the provider, such as an OpenAI model id OpenAI does not know, fails with `decision_rejected` (HTTP 422), not as an outage.
234
+
235
+ Under `lua test` and through the HTTP API, `decision_timeout`, `decision_unavailable` and `decision_disabled` (all HTTP 503) arrive with their own code, message and provider, the same as in deployed code.
236
+
237
+ ## Limits
238
+
239
+ | Limit | Value |
240
+ | --------------------------- | --------- |
241
+ | Questions per call | 64 |
242
+ | Options per choice question | 2 to 64 |
243
+ | Levels per score question | 2 to 10 |
244
+ | Characters per instruction | 4,000 |
245
+ | Characters of state | 120,000 |
246
+ | Images per call | 4 |
247
+ | Size per image | 5 MB |
248
+ | Total image size per call | 6 MB |
249
+ | Characters per name, label | 64 |
250
+ | Minimum timeout | 100 ms |
251
+ | Default timeout | 3,000 ms |
252
+ | Maximum timeout | 15,000 ms |
253
+ | Maximum cache time | 86,400 s |
254
+
255
+ ## Options
256
+
257
+ | Option | Values | Default | Description |
258
+ | --------------- | ------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | `provider` | `'auto'`, `'jev'`, `'openai'` | `'auto'` | Which provider answers, where `auto` lets the platform pick. |
260
+ | `model` | a model id of that provider | provider's | The exact model to use, which must belong to the chosen provider. |
261
+ | `fallback` | `'jev'`, `'openai'`, `'none'` | other one | The provider to try when the first one fails, or `none` to never fall back, and a pinned `provider` never falls back unless you set this. |
262
+ | `region` | `'eu'` | none | Routes the call to OpenAI's EU endpoint. |
263
+ | `zeroRetention` | `true`, `false` | `false` | Routes the call to OpenAI only, and no retention flag is sent with the request. |
264
+ | `minConfidence` | 0 to 1 | `0.7` | Below this confidence a choice or score answer carries `uncertain: true`. |
265
+ | `timeoutMs` | 100 to 15000 | `3000` | How long the whole call may take, fallback included, before it fails with a timeout, and a value outside the range is clamped to it. |
266
+ | `cache` | `{ ttlSeconds }` | none | Reuses the answer to an identical request for this many seconds. |
267
+ | `logState` | `'summary'`, `'full'`, `'none'` | `'summary'` | How much of the state is kept in the decision log. |
268
+
269
+ A pinned `provider` overrides the images, `region` and `zeroRetention` rules.
270
+
271
+ ## Related APIs
272
+
273
+ - [LuaDecision](./LuaDecision.md) declares a named, versioned decision that `Decisions.run` runs.
274
+ - [AI](./AI.md) generates free text when you need prose instead of a judgment.
275
+ - [PreProcessor](./PreProcessor.md) is a common place to route messages with a decision.
276
+ - [LuaWebhook](./LuaWebhook.md) can gate incoming events on a decision.
package/docs/api/Jobs.md CHANGED
@@ -465,6 +465,22 @@ execute: async (job) => {
465
465
  }
466
466
  ```
467
467
 
468
+ ### The thread a job was created in
469
+
470
+ A job created by `Jobs.create` during a chat turn records that turn's full thread id, readable as `job.data.originThreadId` (a string: `base` or `base:suffix`). It is informational and read-only; you never pass it to `Jobs.create`.
471
+
472
+ It is absent for jobs created outside a chat turn (deploy-time `LuaJob`s, webhooks without a chat thread) and for jobs created before it was recorded, so read it as `job.data.originThreadId ?? undefined`.
473
+
474
+ **`user.send()` does not use it.** A message sent from a job still goes to the user's last-interaction channel, and for a webchat-only user it lands on the base `${userId}-${agentId}` thread, not on the conversation thread the job was created in. See [`user.send`](./User.md#usersendmessages).
475
+
476
+ **Example:**
477
+ ```typescript
478
+ execute: async (job) => {
479
+ const threadId: string | undefined = job.data.originThreadId;
480
+ console.log('Created in thread', threadId ?? 'none');
481
+ }
482
+ ```
483
+
468
484
  ## Retry Configuration
469
485
 
470
486
  ```typescript
@@ -0,0 +1,121 @@
1
+ # LuaDecision
2
+
3
+ `LuaDecision` declares a named decision on the agent: a fixed set of questions and the options to ask them with. Code runs it by name with `Decisions.run`, each `lua push` stages a new version of it, and `lua version promote` makes that version current.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import { LuaDecision, defineDecision, Decisions } from 'lua-cli';
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ Declare a decision when several places in your code ask the same questions, or when you want to change the questions or thresholds without redeploying the code that asks them.
14
+
15
+ - The questions live in one place and are checked when you compile.
16
+ - `Decisions.run('triage', state)` always asks the current version's questions.
17
+ - You can write a new version from the dashboard, and publishing an older version rolls back.
18
+ - Every log row and usage row of a run names the decision and its version.
19
+
20
+ ## Configuration
21
+
22
+ ```typescript
23
+ type LuaDecisionConfig = {
24
+ name: string;
25
+ description?: string;
26
+ questions: Record<string, OddsQuestion | ChoiceQuestion | ScoreQuestion>;
27
+ provider?: 'auto' | 'jev' | 'openai';
28
+ model?: string;
29
+ fallback?: 'jev' | 'openai' | 'none';
30
+ region?: 'eu';
31
+ zeroRetention?: boolean;
32
+ minConfidence?: number;
33
+ timeoutMs?: number;
34
+ cache?: { ttlSeconds: number };
35
+ logState?: 'summary' | 'full' | 'none';
36
+ };
37
+ ```
38
+
39
+ | Field | Description |
40
+ | ------------- | --------------------------------------------------------------------------------------------------- |
41
+ | `name` | 1 to 64 letters, digits, underscores or hyphens. Unique on the agent; `Decisions.run` uses it. |
42
+ | `description` | What the decision is for, shown on the dashboard. |
43
+ | `questions` | 1 to 64 named questions, each built with `Decisions.odds`, `Decisions.choice` or `Decisions.score`. |
44
+ | options | The same options as `Decisions.ask`; see the options table in [Decisions](./Decisions.md#options). |
45
+
46
+ Write each question as a `Decisions.odds(...)`, `Decisions.choice(...)` or `Decisions.score(...)` call. The compiler reads them from your source, so their arguments must be values it can see: literals or constants.
47
+
48
+ ## Example
49
+
50
+ ```typescript
51
+ import { LuaAgent, LuaDecision, Decisions } from 'lua-cli';
52
+
53
+ export const triage = new LuaDecision({
54
+ name: 'triage',
55
+ description: 'Route an inbound message to the right team',
56
+ questions: {
57
+ refund: Decisions.odds('Is the customer asking for a refund?'),
58
+ team: Decisions.choice('Which team should handle this?', {
59
+ billing: 'Payments, invoices, refunds and charges.',
60
+ tech: 'Bugs, errors and anything that does not work.',
61
+ sales: 'Pricing, plans and buying more.',
62
+ }),
63
+ },
64
+ minConfidence: 0.7,
65
+ });
66
+
67
+ export const agent = new LuaAgent({
68
+ name: 'support',
69
+ persona: 'You help customers.',
70
+ decisions: [triage],
71
+ });
72
+ ```
73
+
74
+ Then, from any tool, job, webhook or processor of the agent:
75
+
76
+ ```typescript
77
+ const result = await Decisions.run('triage', { message: text });
78
+ const team = result.answers.team;
79
+ if (team.type === 'choice' && !team.uncertain) {
80
+ await routeTo(team.choice);
81
+ }
82
+ ```
83
+
84
+ `defineDecision({...})` takes the same config and is detected the same way.
85
+
86
+ ## Versions
87
+
88
+ `lua push` stages a new version of every decision listed in `decisions`; it does not change what `Decisions.run` asks. `lua push decision --name triage` stages one decision. A staged version becomes current only through `lua version promote` or `lua decisions publish`. Until a decision has a current version, `Decisions.run` throws `decision_not_found` for it.
89
+
90
+ A staged version goes live with the agent version that includes it: run `lua version create`, then `lua version promote <version>`. Promoting an older agent version brings back the decision versions it included, which is how you roll back a pushed decision.
91
+
92
+ A version saved from the dashboard goes live when you publish it, and publishing an older version there rolls back too.
93
+
94
+ Each lua-core pod caches the current version. A promote, a publish, a rename or a delete clears that cache at once, so the change reaches running code without a redeploy.
95
+
96
+ ## From the terminal
97
+
98
+ `lua decisions` lists the named decisions on the agent with their current version. `lua decisions versions triage` shows each version newest first, with its questions, provider and `minConfidence`.
99
+
100
+ `lua decisions run triage --message 'I want a refund'` runs the current version and prints one line per answer, then the provider, rule, model, duration, tokens and fallback. Pass `--state '<json>'` to decide on structured state instead, and `--provider` or `--min-confidence` to override the version's options for that run.
101
+
102
+ `lua decisions publish triage 1.0.1` makes 1.0.1 the current version straight away, which is the quickest rollback. The next `lua version promote` sets it back to the version that agent version included.
103
+
104
+ `lua decisions delete triage` deactivates the decision after asking and keeps its versions. From then on `Decisions.run('triage', ...)` throws `decision_not_found`. Publishing a version of a deleted decision turns it back on.
105
+
106
+ `lua version status` lists each decision with the version the active agent version runs and the version in `lua.skill.yaml`.
107
+
108
+ ## Errors
109
+
110
+ The constructor throws a `DecisionError` with code `decision_invalid` when the name is not valid, there are no questions, or a question was not built with a builder.
111
+
112
+ The builders refuse a malformed question with `decision_invalid` before anything is sent. [Decisions](./Decisions.md#checks-before-the-call) lists the checks.
113
+
114
+ `Decisions.run` throws a `DecisionError` with code `decision_not_found` when the agent does not declare the name, when the decision was deleted, or when it was pushed but never made current. Like every decision error, it carries `decisionId`, the id of the call's log row.
115
+
116
+ Writing a definition answers HTTP 409 on a conflict. That happens when another active decision on the agent already uses the name, on create or rename, and when the version number was already written for that decision. Publishing a deleted decision whose name another decision has taken since also answers 409.
117
+
118
+ ## Related APIs
119
+
120
+ - [Decisions](./Decisions.md) asks questions directly and runs named decisions.
121
+ - [LuaAgent](./LuaAgent.md) lists the decisions in `decisions`.
package/docs/api/User.md CHANGED
@@ -226,7 +226,7 @@ await user.update({
226
226
 
227
227
  Sends messages to the user.
228
228
 
229
- **Where it lands:** `user.send()` targets the user's last-interaction channel (WhatsApp, Teams, SMS and so on). For a user who only chats on webchat, it writes the message to the user's base conversation thread (the `${userId}-${agentId}` root thread) and pushes it live to the webchat widget. It does **not** target a specific workspace conversation; there is no conversation or thread option.
229
+ **Where it lands:** `user.send()` targets the user's last-interaction channel (WhatsApp, Teams, SMS and so on). For a user who only chats on webchat, it writes the message to the user's base conversation thread (the `${userId}-${agentId}` root thread) and pushes it live to the webchat widget. It does **not** target a specific workspace conversation; there is no conversation or thread option. A job's creating thread is readable as `job.data.originThreadId` (see [Jobs.md](./Jobs.md#the-thread-a-job-was-created-in)), but `user.send()` does not target it.
230
230
 
231
231
  **Parameters:**
232
232
 
@@ -266,7 +266,7 @@ await user.send([
266
266
 
267
267
  #### `user.clear()`
268
268
 
269
- Clears all user data.
269
+ Clears all user data, including the user's My work list.
270
270
 
271
271
  **Returns:** `Promise<{ success: boolean }>` - `{ success: true }` once the data is cleared. Throws if the clear fails.
272
272
 
@@ -277,6 +277,57 @@ const user = await User.get();
277
277
  await user.clear();
278
278
  ```
279
279
 
280
+ #### `user.setWorkItems(items)`
281
+
282
+ Publishes this user's **My work** list: the open work an agent lists for them (a ticket they raised or hold, an approval waiting on them). Their client shows it beside the conversation, for example Frontline's "My work" tab. Each call replaces the whole list.
283
+
284
+ **Parameters:** `items: WorkItem[]`, at most 100.
285
+
286
+ | Field | Type | Notes |
287
+ | ------------- | ---------------- | ----------------------------------------------------------------------- |
288
+ | `id` | `string` | Unique in the list, ≤ 200 chars (e.g. a ticket number) |
289
+ | `title` | `string` | One line the user recognises, ≤ 120 chars |
290
+ | `group` | `string` | Display group you choose, ≤ 40 chars ("Waiting on you", "Your tickets") |
291
+ | `needsAction` | `boolean?` | The user's own action is needed; listed first and highlighted |
292
+ | `status` | `string?` | Short label, ≤ 40 chars ("In progress") |
293
+ | `updatedAt` | `string \| Date` | When this work last changed |
294
+ | `action` | `string` | Message the client sends when the item is tapped, ≤ 200 chars |
295
+
296
+ **Returns:** `Promise<void>`. Throws an error naming the first invalid item before anything is written, or `Failed to set work items` if the write fails.
297
+
298
+ **Rules:**
299
+
300
+ - **`action` is a show/open request, never a decision.** Use "Show MNT-00123", not "Approve MNT-00123". A tap must not act on a list that may be minutes old; ask for decisions in the conversation instead.
301
+ - **Publish whenever the work changes, for everyone it affects.** That means the raiser, the assignee and the next approver. Use `User.get({ phone })` or `User.get(userId)` to address each of them.
302
+ - **Add a scheduled job (every ~30 min)** that rebuilds the lists of recently active users. It catches any write that failed.
303
+ - **Leave closed or cancelled work out.** An empty list (`[]`) means "nothing waiting".
304
+ - **The list is kept out of `user.data`.** `update()`, `save()` and `patch()` never write it, so a stale read can't overwrite a newer list. `user.clear()` removes it.
305
+
306
+ **Example:**
307
+
308
+ ```typescript
309
+ import { User } from 'lua-cli';
310
+
311
+ // After assigning MNT-00123 to a technician:
312
+ const technician = await User.get({ phone: '+254700000000' });
313
+ await technician?.setWorkItems([
314
+ {
315
+ id: 'MNT-00123',
316
+ title: 'MNT-00123 · Fryer not heating',
317
+ group: 'Assigned to you',
318
+ status: 'In progress',
319
+ updatedAt: new Date(),
320
+ action: 'Show MNT-00123',
321
+ },
322
+ ]);
323
+ ```
324
+
325
+ #### `user.clearWorkItems()`
326
+
327
+ Removes this user's My work list. Their client then shows no list from this agent.
328
+
329
+ **Returns:** `Promise<void>`
330
+
280
331
  #### `user.save()`
281
332
 
282
333
  Saves current user data state.
@@ -20,6 +20,18 @@ Inside a sandbox (tools, jobs, webhooks, workflow code steps) `Workflows` is als
20
20
  - `get` / `list` honour output visibility: a restricted run comes back with `restricted: true` and no `output` — never an error.
21
21
  - From a workflow code step, `Workflows.start` creates a **detached** run (no parent link). Use the `.workflow(...)` builder node when the parent should wait.
22
22
 
23
+ ## Concurrency per correlation key
24
+
25
+ `concurrencyPolicy: 'forbid'` alone is workflow-wide: one in-flight run blocks every other start of that workflow. Adding `concurrencyKey: 'correlationKey'` (set on the push API request next to `concurrencyPolicy`; the CLI / SDK declaration follows) narrows the guard to runs that share a `correlationKey`:
26
+
27
+ - A start is refused only while a **non-terminal** run (`queued`, `running`, `cancellation_requested`, `gated`, `suspended`, `waiting`) of the same workflow carries the **same** `correlationKey`. `completed`, `failed`, `cancelled`, `abandoned` and `timed_out` runs free the key.
28
+ - Starts with **different** keys run in parallel (for example one release per agent).
29
+ - Starts with **no** `correlationKey` share one null bucket: a second keyless start is refused while a keyless run is in flight. A keyless start and a keyed start never block each other.
30
+ - Without `concurrencyKey` nothing changes: the guard stays workflow-wide.
31
+ - `concurrencyKey` requires `concurrencyPolicy: 'forbid'`; otherwise the push is a 400 `VALIDATION_FAILED` (`concurrency-key-requires-forbid`).
32
+ - The refusal is a 409 `RUNS_IN_FLIGHT` whose body carries `blockingRunId`, `blockingStatus`, `concurrencyKey` and the colliding `correlationKey` (absent for the null bucket).
33
+ - A scheduled fire that collides on its rendered `correlationKeyTemplate` key records `skipped_overlap`; a fire with a different key dispatches.
34
+
23
35
  ## Methods
24
36
 
25
37
  ### `Workflows.start(nameOrId, input?, opts?)`
@@ -2,7 +2,7 @@
2
2
 
3
3
  _Source of truth: workflows-spec 07 §7.1. This page is the developer summary; the spec is normative._
4
4
 
5
- `schedule: { type:'cron', expression: '0 9 * * 1', timezone: 'Europe/London' }` on `createWorkflow` fires runs on the cron grid; `scheduleInput` is the run input. `concurrencyPolicy:'forbid'` skips a fire while a run is in flight (the skip is recorded, never queued). Manage with `lua workflows activate` / `deactivate`, or create and drive a schedule from the CLI without a `schedule:` block — `lua workflows schedules create|pause|resume|patch|delete` below; a re-enable with `--backfill-now` starts the missed occurrences (deduplicated on `backfill:<workflowId>:<occurrenceIso>`).
5
+ `schedule: { type:'cron', expression: '0 9 * * 1', timezone: 'Europe/London' }` on `createWorkflow` fires runs on the cron grid; `scheduleInput` is the run input. `concurrencyPolicy:'forbid'` skips a fire while a run is in flight (the skip is recorded, never queued). With `concurrencyKey:'correlationKey'` a fire is skipped only when a run with the same rendered `correlationKeyTemplate` key is in flight; a fire with no key shares one null bucket. Manage with `lua workflows activate` / `deactivate`, or create and drive a schedule from the CLI without a `schedule:` block — `lua workflows schedules create|pause|resume|patch|delete` below; a re-enable with `--backfill-now` starts the missed occurrences (deduplicated on `backfill:<workflowId>:<occurrenceIso>`).
6
6
 
7
7
  ## Schedule health
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.46.0",
3
+ "version": "3.48.0",
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",
@@ -20,7 +20,7 @@
20
20
  "inquirer": "^12.9.6",
21
21
  "stripe": "^17.5.0",
22
22
  "js-yaml": "^4.1.0",
23
- "lua-cli": "^3.46.0",
23
+ "lua-cli": "^3.48.0",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"