@lunnoa/client 2.4.0 → 3.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunnoa/client",
3
- "version": "2.4.0",
3
+ "version": "3.0.0",
4
4
  "description": "TypeScript SDK for building applications on top of Lunnoa Automate — typed client for the Public API (agents, workflows, executions, entities, knowledge, queues), SSE agent chat streaming, and deployment-specific codegen",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -24,7 +24,8 @@
24
24
  },
25
25
  "files": [
26
26
  "dist",
27
- "skills"
27
+ "skills",
28
+ "schemas"
28
29
  ],
29
30
  "scripts": {
30
31
  "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
@@ -34,6 +35,7 @@
34
35
  "typecheck": "tsc --noEmit",
35
36
  "check:codegen-sample": "node --experimental-strip-types scripts/check-codegen-sample.ts && tsc --noEmit -p scripts/tsconfig.codegen-sample.json",
36
37
  "test:acceptance": "tsx scripts/acceptance-soak.ts",
38
+ "test:sync-acceptance": "tsx scripts/sync-acceptance.ts",
37
39
  "prepare": "node -e \"for (const d of ['.cursor/skills', '.claude/skills']) require('fs').cpSync('skills', d, { recursive: true, force: true })\"",
38
40
  "prepublishOnly": "pnpm run build",
39
41
  "release": "semantic-release"
@@ -0,0 +1,128 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lunnoa.com/schemas/agent.schema.json",
4
+ "title": "Lunnoa agent definition",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["kind", "slug", "name", "instructions"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "kind": { "const": "agent" },
11
+ "slug": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
12
+ "name": { "type": "string" },
13
+ "description": { "type": "string" },
14
+ "model": { "type": "string" },
15
+ "aiConnection": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": ["id"],
19
+ "properties": { "id": { "type": "string" } }
20
+ },
21
+ "instructions": {
22
+ "oneOf": [
23
+ { "type": "string" },
24
+ {
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "required": ["file"],
28
+ "properties": { "file": { "type": "string" } }
29
+ }
30
+ ]
31
+ },
32
+ "settings": { "$ref": "#/$defs/settings" },
33
+ "tools": {
34
+ "type": "array",
35
+ "items": { "$ref": "#/$defs/tool" }
36
+ },
37
+ "workflows": { "type": "array", "items": { "$ref": "#/$defs/slugOrId" } },
38
+ "agents": { "type": "array", "items": { "$ref": "#/$defs/slugOrId" } },
39
+ "connections": { "type": "array", "items": { "$ref": "#/$defs/idRef" } },
40
+ "knowledge": { "type": "array", "items": { "$ref": "#/$defs/idRef" } },
41
+ "variables": { "type": "array", "items": { "$ref": "#/$defs/idRef" } },
42
+ "objects": { "type": "array", "items": { "$ref": "#/$defs/idRef" } },
43
+ "objectTypes": { "type": "array", "items": { "$ref": "#/$defs/idRef" } },
44
+ "mcpServers": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["id"],
50
+ "properties": {
51
+ "id": { "type": "string" },
52
+ "connectionId": { "type": "string" }
53
+ }
54
+ }
55
+ },
56
+ "a2aAgents": { "type": "array", "items": { "$ref": "#/$defs/idRef" } }
57
+ },
58
+ "$defs": {
59
+ "idRef": {
60
+ "type": "object",
61
+ "additionalProperties": false,
62
+ "required": ["id"],
63
+ "properties": { "id": { "type": "string" } }
64
+ },
65
+ "slugOrId": {
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "minProperties": 1,
69
+ "properties": {
70
+ "slug": { "type": "string" },
71
+ "id": { "type": "string" }
72
+ }
73
+ },
74
+ "tool": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["key", "id", "name", "input"],
78
+ "properties": {
79
+ "key": { "type": "string" },
80
+ "id": { "type": "string" },
81
+ "appId": { "type": "string" },
82
+ "connection": { "$ref": "#/$defs/idRef" },
83
+ "name": { "type": "string" },
84
+ "description": { "type": "string" },
85
+ "input": { "type": "object" }
86
+ }
87
+ },
88
+ "settings": {
89
+ "type": "object",
90
+ "additionalProperties": false,
91
+ "properties": {
92
+ "temperature": { "type": "number" },
93
+ "topP": { "type": "number" },
94
+ "maxOutputTokens": { "type": ["number", "null"] },
95
+ "frequencyPenalty": { "type": "number" },
96
+ "presencePenalty": { "type": "number" },
97
+ "seed": { "type": ["number", "null"] },
98
+ "maxRetries": { "type": "number" },
99
+ "maxToolRoundtrips": { "type": "number" },
100
+ "messageLookbackLimit": { "type": "number" },
101
+ "taskNamingInstructions": { "type": ["string", "null"] },
102
+ "enabledBuiltInTools": { "type": "array", "items": { "type": "string" } },
103
+ "enabledPlatformTools": { "type": "array", "items": { "type": "string" } },
104
+ "enabledToolPolicies": { "type": "array", "items": { "type": "string" } },
105
+ "forcedTools": {},
106
+ "suggestedPrompts": { "type": "array", "items": { "type": "string" } },
107
+ "dynamicActionDiscoveryEnabled": { "type": "boolean" },
108
+ "dynamicMcpDiscoveryEnabled": { "type": "boolean" },
109
+ "dynamicSelfInstructionsEnabled": { "type": "boolean" },
110
+ "memoryEnabled": { "type": "boolean" },
111
+ "memoryScope": { "type": "string" },
112
+ "memoryRetentionDays": { "type": "number" },
113
+ "enablePlanning": { "type": "boolean" },
114
+ "requirePlanApproval": { "type": "boolean" },
115
+ "enableToolResultOffload": { "type": "boolean" },
116
+ "toolResultOffloadThresholdBytes": { "type": "number" },
117
+ "longHorizonMode": { "type": "boolean" },
118
+ "longHorizonMaxSteps": { "type": ["number", "null"] },
119
+ "longHorizonMaxTokensPerRun": { "type": ["number", "null"] },
120
+ "piiRoutingMode": { "type": "string" },
121
+ "piiFallbackConnectionId": { "type": ["string", "null"] },
122
+ "piiFallbackModelId": { "type": ["string", "null"] },
123
+ "webAccess": { "type": ["boolean", "null"] },
124
+ "phoneAccess": { "type": ["boolean", "null"] }
125
+ }
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,156 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lunnoa.com/schemas/workflow.schema.json",
4
+ "title": "Lunnoa workflow definition",
5
+ "oneOf": [
6
+ { "$ref": "#/$defs/steps" },
7
+ { "$ref": "#/$defs/graph" }
8
+ ],
9
+ "$defs": {
10
+ "idRef": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["id"],
14
+ "properties": { "id": { "type": "string" } }
15
+ },
16
+ "retrySettings": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": ["enabled", "maxAttempts", "delaySeconds"],
20
+ "properties": {
21
+ "enabled": { "type": "boolean" },
22
+ "maxAttempts": { "type": "number" },
23
+ "delaySeconds": { "type": "number" }
24
+ }
25
+ },
26
+ "continueOnFailure": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["enabled"],
30
+ "properties": { "enabled": { "type": "boolean" } }
31
+ },
32
+ "use": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "required": ["id"],
36
+ "properties": {
37
+ "id": { "type": "string" },
38
+ "appId": { "type": "string" },
39
+ "connection": { "$ref": "#/$defs/idRef" }
40
+ }
41
+ },
42
+ "step": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["key", "name", "use", "input"],
46
+ "properties": {
47
+ "key": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]*$" },
48
+ "name": { "type": "string" },
49
+ "description": { "type": "string" },
50
+ "use": { "$ref": "#/$defs/use" },
51
+ "input": { "type": "object" },
52
+ "retrySettings": { "$ref": "#/$defs/retrySettings" },
53
+ "continueOnFailure": { "$ref": "#/$defs/continueOnFailure" },
54
+ "paths": {
55
+ "type": "array",
56
+ "items": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["label", "steps"],
60
+ "properties": {
61
+ "label": { "type": "string" },
62
+ "when": { "type": "array" },
63
+ "steps": {
64
+ "type": "array",
65
+ "items": { "$ref": "#/$defs/step" }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ },
72
+ "steps": {
73
+ "type": "object",
74
+ "additionalProperties": false,
75
+ "required": ["kind", "format", "slug", "name", "trigger", "steps"],
76
+ "properties": {
77
+ "$schema": { "type": "string" },
78
+ "kind": { "const": "workflow" },
79
+ "format": { "const": "steps" },
80
+ "slug": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
81
+ "name": { "type": "string" },
82
+ "description": { "type": "string" },
83
+ "trigger": {
84
+ "type": "object",
85
+ "additionalProperties": false,
86
+ "required": ["id", "name"],
87
+ "properties": {
88
+ "id": { "type": "string" },
89
+ "appId": { "type": "string" },
90
+ "connection": { "$ref": "#/$defs/idRef" },
91
+ "input": { "type": "object" },
92
+ "inputs": { "type": "array" },
93
+ "name": { "type": "string" },
94
+ "description": { "type": "string" }
95
+ }
96
+ },
97
+ "steps": {
98
+ "type": "array",
99
+ "minItems": 1,
100
+ "items": { "$ref": "#/$defs/step" }
101
+ }
102
+ }
103
+ },
104
+ "graphNode": {
105
+ "type": "object",
106
+ "required": ["key", "nodeType", "appId", "name", "value"],
107
+ "properties": {
108
+ "id": { "type": "string" },
109
+ "key": { "type": "string" },
110
+ "nodeType": { "enum": ["trigger", "action"] },
111
+ "appId": { "type": "string" },
112
+ "actionId": { "type": "string" },
113
+ "triggerId": { "type": "string" },
114
+ "name": { "type": "string" },
115
+ "description": { "type": "string" },
116
+ "value": { "type": "object" },
117
+ "retrySettings": { "type": "object" },
118
+ "continueOnFailure": { "type": "object" }
119
+ }
120
+ },
121
+ "graph": {
122
+ "type": "object",
123
+ "additionalProperties": false,
124
+ "required": ["kind", "format", "slug", "name", "nodes", "edges"],
125
+ "properties": {
126
+ "$schema": { "type": "string" },
127
+ "kind": { "const": "workflow" },
128
+ "format": { "const": "graph" },
129
+ "slug": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
130
+ "name": { "type": "string" },
131
+ "description": { "type": "string" },
132
+ "nodes": {
133
+ "type": "array",
134
+ "minItems": 1,
135
+ "items": { "$ref": "#/$defs/graphNode" }
136
+ },
137
+ "edges": {
138
+ "type": "array",
139
+ "items": {
140
+ "type": "object",
141
+ "additionalProperties": false,
142
+ "required": ["source", "target"],
143
+ "properties": {
144
+ "id": { "type": "string" },
145
+ "source": { "type": "string" },
146
+ "target": { "type": "string" },
147
+ "sourceHandle": { "type": "string" },
148
+ "targetHandle": { "type": "string" },
149
+ "type": { "type": "string" }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
@@ -4,7 +4,7 @@ description: >-
4
4
  Builds custom frontends and backends on top of a Lunnoa Automate deployment
5
5
  using the @lunnoa/client TypeScript SDK. App login is always a Lunnoa user
6
6
  (createBrowserClient + lunnoa.auth.login / refresh / SSO). lna_ API keys are
7
- for the CLI only (discovery, codegen, deploy), never for the app session and
7
+ for the CLI only (discovery, codegen, deploy, pull, push), never for the app session and
8
8
  never in a browser. Covers discovery of the deployment's agents, workflows, and entity
9
9
  types, running the deployment codegen first for typed accessors, querying
10
10
  and creating entities, triggering workflows and polling executions,
@@ -52,7 +52,7 @@ Run these before inventing resources or shipping UI code:
52
52
  1. **Auth** — Lunnoa user JWT via `createBrowserClient` + `lunnoa.auth.login`
53
53
  (or SSO). **Never** call Lunnoa as the app's users with an `lna_` key, and
54
54
  **never** put `lna_` in the browser (SDK throws when `window` exists).
55
- `lna_` keys are for CLI discovery, codegen, and deploy only.
55
+ `lna_` keys are for CLI discovery, codegen, deploy, pull, and push only.
56
56
  2. **Codegen** — When building typed UIs, run
57
57
  `npx @lunnoa/client codegen` against the deployment first.
58
58
  3. **Discover before inventing** — prefer the CLI (JSON for agents), then
@@ -67,10 +67,16 @@ Run these before inventing resources or shipping UI code:
67
67
  Do not invent catalogue action ids or model ids. `connections list` is
68
68
  app connections (Gmail, HTTP). `ai-connections list` is the model
69
69
  provider connection and the `models[].id` values `defineAgent` accepts.
70
- 4. **Run vs deploy** — Ad-hoc: `actions.run` / `runs.start` /
71
- `workflows.execute`. Durable shared automation: CLI
72
- `npx @lunnoa/client workflows|agents deploy`. Upsert is by slug
73
- inside the workspace on the API key. URL and API key are enough.
70
+ 4. **Run vs save** — Ad-hoc: `actions.run` / `runs.start` /
71
+ `workflows.execute`. Durable agents and workflows default to JSON in
72
+ `lunnoa/` (`npx lunnoa-client status`, then `pull`, then `push`).
73
+ That sets `codeSource: synced`. TypeScript
74
+ `npx @lunnoa/client workflows|agents deploy` is only for a definition
75
+ that needs code. That sets `codeSource: generated` and locks the UI.
76
+ A `.json` deploy is a push. `codeSource` replaces `managedByCode` on
77
+ agents and workflows. Tables and entity types still use
78
+ `managedByCode`. Upsert is by slug inside the workspace on the API
79
+ key. See section 5d.
74
80
  5. **Toolkit boundary** — Client `defineAction` ≠ toolkit `createAction`.
75
81
 
76
82
  ---
@@ -85,7 +91,7 @@ app does not keep its own user directory and does not call Lunnoa with an
85
91
  | Caller | Credential |
86
92
  |---|---|
87
93
  | **Person using the app** | `createBrowserClient({ baseUrl })` + `lunnoa.auth.login` (password, 2FA, or SSO). Permissions, agent shares, and task history are that user's. |
88
- | **CLI (codegen, discovery, deploy)** | `lna_` API key in `.env` / `--api-key`. Not the app session. |
94
+ | **CLI (codegen, discovery, deploy, pull, push)** | `lna_` API key in `.env` / `--api-key`. Not the app session. |
89
95
 
90
96
  Hard rules:
91
97
 
@@ -455,6 +461,42 @@ Wire step settings in code with `ref`. Do not leave them for the builder.
455
461
  node id across deploys, so run history and builder mappings stay attached.
456
462
  The trigger keeps its id while its type and settings stay the same.
457
463
 
464
+ ### Branches (paths)
465
+
466
+ Put `paths` on the **last** step of a list. Steps after the branch go inside
467
+ each path. Paths do not merge back together.
468
+
469
+ ```ts
470
+ const approval = defineAction({ id: 'flow-control_action_request-approval', input: { /* from tools get */ } });
471
+ const split = defineAction({ id: 'flow-control_action_conditional-paths' });
472
+
473
+ steps: [
474
+ { key: 'review', use: approval, paths: [
475
+ { label: 'Approved', steps: [{ key: 'apply', use: applyChange }] },
476
+ { label: 'Rejected', steps: [{ use: notify, input: { text: ref.step('review', 'comment') } }] },
477
+ ] },
478
+ ]
479
+
480
+ // Conditional Paths: groups in `when` are OR-ed, conditions in a group AND-ed.
481
+ { key: 'route', use: split, paths: [
482
+ { label: 'Large', when: [[{ field: ref.step('load', 'amount'), condition: 'is_greater_than', value: '1000' }]], steps: [...] },
483
+ { label: 'Other', steps: [...] }, // no `when`: always taken
484
+ ] }
485
+ ```
486
+
487
+ - `paths` works on Request Approval (at most two: approved first, then
488
+ rejected; labels `Approved` / `Rejected`), Conditional Paths (with `when`),
489
+ and Manually Decide Path (no `when`).
490
+ - `when` conditions: `field` is exactly one `ref.trigger` / `ref.step`.
491
+ `condition` is one of `PATH_CONDITIONS` (equals, contains, is_empty,
492
+ is_true, is_greater_than, is_less_than, matches_regex, is_in_the_past, ...).
493
+ `value` is text; omit it for exists / is_empty / is_true style conditions.
494
+ - Conditional Paths takes every path whose `when` matches. A path without
495
+ `when` always runs. For if/else, give the else path the opposite condition.
496
+ - Steps on a path can `ref.step` the branching step and earlier steps on
497
+ the same path, not steps on another path.
498
+ - `runs.start` rejects `paths`. Deploy, then execute the workflow.
499
+
458
500
  Discover the catalogue **before** writing `defineAction` (do not guess ids):
459
501
 
460
502
  ```bash
@@ -486,11 +528,138 @@ npx @lunnoa/client workflows deploy ./ping.ts --url … --api-key …
486
528
  npx @lunnoa/client agents deploy ./triage.ts --url … --api-key …
487
529
  ```
488
530
 
531
+ Those two commands deploy TypeScript and set `codeSource: generated`
532
+ (locked in the UI). For the JSON round-trip, use section 5d. The binary
533
+ name is `lunnoa-client`, so `npx lunnoa-client` and `npx @lunnoa/client`
534
+ are the same command.
535
+
489
536
  `defineAction` binds catalogue actions only. Toolkit `createAction` is a
490
537
  different job (in-platform apps).
491
538
 
492
539
  ---
493
540
 
541
+ ## 5d. Sync JSON definitions with the UI
542
+
543
+ Default for a saved agent or workflow: a JSON file in `lunnoa/`, then
544
+ `push`. Use a TypeScript `defineAgent` / `defineWorkflow` file only when
545
+ the definition needs code (you also pass it to `runs.start`, or the
546
+ tools are built in code). TypeScript deploy sets `codeSource: generated`
547
+ and the UI badge **Managed by code**. The UI will not edit it. Deploy
548
+ the TypeScript file again to change it.
549
+
550
+ `codeSource` (`none` | `synced` | `generated`) replaces `managedByCode`
551
+ on agents and workflows. Tables and entity types still use
552
+ `managedByCode`.
553
+
554
+ Folder (default `./lunnoa`, or `definitionsDir` in `lunnoa.config.*`, or
555
+ `--dir`):
556
+
557
+ ```text
558
+ lunnoa/agents/<slug>.json
559
+ lunnoa/agents/<slug>.instructions.md
560
+ lunnoa/workflows/<slug>.json
561
+ lunnoa/lunnoa.lock.json
562
+ ```
563
+
564
+ The JSON holds `"instructions": { "file": "<slug>.instructions.md" }`.
565
+ `pull` writes that split. A hand-written file may use a string. Every
566
+ JSON file starts with `$schema`, a relative path to
567
+ `node_modules/@lunnoa/client/schemas/agent.schema.json` or
568
+ `workflow.schema.json` (draft 2020-12). The CLI strips `$schema` before
569
+ sending and checks the file shape (slug, instructions, `use.id`, unique
570
+ keys, one trigger on a graph). Commit `lunnoa.lock.json`. It stores id,
571
+ revision, and file hash per deployment key
572
+ `<base url>#<workspace id>`.
573
+
574
+ Not in the file, and push does not change them: credentials (connection
575
+ ids only), canvas positions, the workflow on/off switch (`isActive`),
576
+ profile image, shares, memories, skills. Pull warns, and still writes
577
+ the file, when an input field name looks like a secret
578
+ (`authorization`, `token`, `secret`, `password`, `apiKey`, `bearer`,
579
+ `cookie`, `privateKey`, …) and the value is a literal string. Keep that
580
+ value on a Lunnoa connection.
581
+
582
+ Workflow pull writes `"format": "steps"` when a trigger plus a step list
583
+ round-trips. It writes `"format": "graph"` when steps would drop
584
+ something (paths that join, a for-each loop). Edit the format pull
585
+ wrote.
586
+
587
+ Commands (reads `LUNNOA_URL` and `LUNNOA_API_KEY` from the environment
588
+ or `./.env`):
589
+
590
+ ```bash
591
+ npx lunnoa-client status
592
+ npx lunnoa-client status --json
593
+ npx lunnoa-client pull
594
+ npx lunnoa-client pull <slug>
595
+ npx lunnoa-client pull agents/<slug>
596
+ npx lunnoa-client pull workflows/<slug>
597
+ npx lunnoa-client pull --all
598
+ npx lunnoa-client push
599
+ npx lunnoa-client push <path-or-slug>
600
+ ```
601
+
602
+ Run `status` before editing. Run `pull` before `push` when the UI has
603
+ moved. `pull` with no slug updates lock entries for this deployment.
604
+ `pull --all` fetches every agent and workflow in the workspace. `push`
605
+ with no path sends every JSON file, workflows first, then agents, with
606
+ `codeSource: synced` and `baseRevision` from the lock. On success it
607
+ rewrites the file from the stored definition and updates the lock.
608
+
609
+ `status` columns: `LOCAL` is `unchanged`, `modified`, `new`, or
610
+ `missing`. `REMOTE` is `unchanged`, `changed in UI`, or
611
+ `missing in Lunnoa`. `CONFLICT` is true when both sides differ from the
612
+ lock. `--json` prints `{ kind, slug, local, remote, conflict }`.
613
+ `remote` uses the words `changed in UI`.
614
+
615
+ How to read a conflict:
616
+
617
+ - Pull exits 1 before it writes anything. The message starts with
618
+ `Local and server both changed. Run pull --force to overwrite.` Then
619
+ a diff: `--- local` / `+++ server`. `-` lines are the file. `+` lines
620
+ are the server. Secret values in that diff are `***`.
621
+ - Push exits 1 and prints the server text. Stale lock:
622
+ `Changed in the UI since your last pull. Run pull first. (currentRevision: …)`.
623
+ First push of a file whose slug already exists and is not in the lock:
624
+ `A workflow with slug "<slug>" already exists in Lunnoa. Run pull first, or push with --force.`
625
+ Agents use `agent` in that sentence. Pull that slug, then push.
626
+ - Ask the human before `--force`. `pull --force` overwrites the local
627
+ edit with the UI. `push --force` overwrites the UI with the file.
628
+ `agents deploy file.ts --force` does not send the flag. A `.json`
629
+ deploy is a push and does.
630
+
631
+ To turn a generated resource into a synced one: `pull` the slug, then
632
+ `push` the JSON. After that the UI can edit it, unless the workspace
633
+ switch **Lock code-synced resources** is on (Admin Panel, workspace,
634
+ Settings, LLM Audit Policy card). That switch makes synced resources
635
+ read-only and shows **Managed by code**. Badges: **Synced with code**,
636
+ **Changed in UI, not yet in code**, **Managed by code**. A successful
637
+ push clears **Changed in UI, not yet in code**. The docs CI recipe runs
638
+ that push on main.
639
+
640
+ App code may pass a parsed file to the factories. `$schema` is stripped.
641
+ `defineAgent` keeps `instructions: { file }` as that object. The CLI
642
+ reads the markdown on push. Do not upsert from the app for this loop.
643
+
644
+ ```typescript
645
+ import { readFileSync } from 'node:fs';
646
+ import { defineAgent, defineWorkflow } from '@lunnoa/client';
647
+
648
+ const agent = defineAgent(
649
+ JSON.parse(readFileSync('lunnoa/agents/triage.json', 'utf8')),
650
+ );
651
+ const workflow = defineWorkflow(
652
+ JSON.parse(readFileSync('lunnoa/workflows/sync-orders.json', 'utf8')),
653
+ );
654
+ ```
655
+
656
+ Human detail: Lunnoa Docs, Develop, TypeScript client SDK, Sync with the
657
+ UI (`/guides/develop/client/sync`), including the CI pull request and
658
+ the main-branch push. Do not add that workflow to a brand-new repo that
659
+ has no secrets.
660
+
661
+ ---
662
+
494
663
  ## 6. Recipe: resume a NEEDS_INPUT execution from a custom form
495
664
 
496
665
  Workflows can pause for human input. The waiting step appears in
@@ -532,7 +701,7 @@ for await (const chunk of stream) {
532
701
  }
533
702
  ```
534
703
 
535
- A turn from an app runs in **preview**: 10 model rounds, so the agent can call tools a few times and still send a final reply. The SDK sends `chatMode: 'preview'` for you. Do not pass `chatMode: 'builder'` from an app. That is the agent-editor interview: 4 rounds, and it stops a tool-using agent halfway. The workflow then fails with "Agent did not generate a response" even when a tool already saved the data. A workflow Message Agent step uses the same preview budget. For a job that chains many tool calls (read every record, then write a result), turn on **Long-horizon mode** on that agent in Lunnoa. That raises the budget to 50 rounds. `defineAgent` cannot set the switch, and a later deploy leaves it as you set it.
704
+ A turn from an app runs in **preview**: 10 model rounds, so the agent can call tools a few times and still send a final reply. The SDK sends `chatMode: 'preview'` for you. Do not pass `chatMode: 'builder'` from an app. That is the agent-editor interview: 4 rounds, and it stops a tool-using agent halfway. The workflow then fails with "Agent did not generate a response" even when a tool already saved the data. A workflow Message Agent step uses the same preview budget. For a job that chains many tool calls (read every record, then write a result), turn on **Long-horizon mode** on that agent in Lunnoa. That raises the budget to 50 rounds. A JSON definition includes `settings.longHorizonMode`, and a push writes it. A TypeScript `defineAgent` does not set the switch, and a later TypeScript deploy leaves it as you set it.
536
705
 
537
706
  - **React apps:** prefer `useAgentChat` from `@lunnoa/client/react`, which wraps AI SDK `useChat` + `DefaultChatTransport`
538
707
  pointed at
@@ -630,7 +799,7 @@ with emails (capped; not Azure object IDs).
630
799
  3. Call `discovery.enabledFeatures()` and hide unsupported areas.
631
800
  4. Build forms/tables from `attributeSchema` / `customInputConfig` (or the generated types).
632
801
  5. Render workflow progress from `executionPath`; handle `NEEDS_INPUT` with `submitInput`.
633
- 6. For single-action calls, use `actions.run`; for linear multi-step, use `runs.start` / `defineWorkflow`. Deploy with `npx @lunnoa/client workflows|agents deploy`. Discover actions via `npx @lunnoa/client tools list|get|search` (or `workflowApps.list()`). Discover triggers via `apps get <appId> --json` and pass `defineTrigger` on `defineWorkflow` (omit it for Manually Run with no fields). `runs.start` does not save the trigger. Pass trigger inputs and earlier step outputs into step settings with `ref.trigger` / `ref.step` (key the source step). Discover agent models via `npx @lunnoa/client ai-connections list --json` (or `aiConnections.list()`).
802
+ 6. For single-action calls, use `actions.run`; for linear multi-step, use `runs.start` / `defineWorkflow`. Save agents and workflows as JSON in `lunnoa/` and use `npx lunnoa-client status`, then `pull`, then `push` (section 5d). Ask the human before `--force`. Use `npx @lunnoa/client workflows|agents deploy` for a TypeScript file that needs code (`codeSource: generated`). Discover actions via `npx @lunnoa/client tools list|get|search` (or `workflowApps.list()`). Discover triggers via `apps get <appId> --json` and pass `defineTrigger` on `defineWorkflow` (omit it for Manually Run with no fields). `runs.start` does not save the trigger. Pass trigger inputs and earlier step outputs into step settings with `ref.trigger` / `ref.step` (key the source step). Branch with `paths` on Request Approval or Conditional Paths (last step only). Discover agent models via `npx @lunnoa/client ai-connections list --json` (or `aiConnections.list()`).
634
803
  7. For Request Approval nodes, use `approvals.inbox` / `approvals.decide` (or `executions.submitApproval`).
635
804
  8. Chat: stream via SSE, handle resume (reconnects) and stop, load history from the task.
636
805
  9. Handle `LunnoaApiError`: 401 → re-auth; 403 → hide the capability; 429 → back off.
@@ -74,7 +74,7 @@ interrogation.
74
74
  | End users | Auth |
75
75
  |---|---|
76
76
  | People using the app | Lunnoa user JWT: `createBrowserClient` + `lunnoa.auth.login` (or SSO). Provision missing people with invite, SSO, or SCIM first. |
77
- | Coding agent / CLI | `LUNNOA_API_KEY` for discovery, codegen, and deploy. Not the app session. |
77
+ | Coding agent / CLI | `LUNNOA_API_KEY` for discovery, codegen, deploy, pull, and push. Not the app session. |
78
78
  | Anonymous embed chat (no full login) | Out of scope for v1 client path; scoped embed tokens are a later platform bet |
79
79
 
80
80
  ---
@@ -86,7 +86,7 @@ interrogation.
86
86
  | One catalogue call | `actions.run` | One-step Execution (`source: SDK`) |
87
87
  | Linear multi-step, experiment | `runs.start` / `defineWorkflow` then run | One Execution; optional later deploy |
88
88
  | Reusable automation + triggers | Saved **Workflow** (`defineWorkflow` + `defineTrigger`, or the UI) | Discover trigger ids with `apps get <appId> --json`. Omit `trigger` for Manually Run with no fields. `runs.start` ignores the trigger. Wire step settings to trigger fields and earlier steps with `ref.trigger` / `ref.step`. |
89
- | Branches (approve / reject, if/else) | Builder in Lunnoa | Code deploys build straight lines only. A later code deploy replaces builder edits. |
89
+ | Branches (approve / reject, if/else) | `defineWorkflow` with `paths` on Request Approval or Conditional Paths | The steps format does not merge paths. Repeat shared steps in each path, or build the join in the UI and `pull`: the file becomes graph JSON and `push` keeps the join. A TypeScript deploy (`codeSource: generated`) replaces the stored graph and locks the UI. |
90
90
  | Reasoning + tools + conversation | **Agent** | Tools = workflows, actions, knowledge |
91
91
  | Structured CRUD / portals | **Entities** + schemas | Codegen for typed accessors |
92
92
  | Reference / lookup data | **Tables** | `tables.upsertBySlug`; workflows use the Tables Lookup action |
@@ -131,8 +131,13 @@ interrogation.
131
131
  as a fixed value, "from trigger `caseId`", or "from step `load` →
132
132
  `rows.0.status`". The build uses `ref.trigger` / `ref.step` for these, so
133
133
  "connect it in the builder" is not a fallback for data wiring.
134
- 5. **Durable vs ephemeral:** experiments → `runs.start`; shared production
135
- automation → CLI `workflows deploy` / `agents deploy` by slug.
134
+ 5. **Durable vs ephemeral:** experiments → `runs.start`. Shared production
135
+ agents and workflows → JSON in `lunnoa/` (`npx lunnoa-client status`,
136
+ `pull`, `push`), `codeSource: synced`. TypeScript
137
+ `workflows deploy` / `agents deploy` only when the definition needs
138
+ code (`codeSource: generated`, locked in the UI). A `.json` deploy is
139
+ a push. `codeSource` replaces `managedByCode` on agents and workflows.
140
+ Ask the human before `--force`. Commit `lunnoa/lunnoa.lock.json`.
136
141
  6. **UI is headless.** `@lunnoa/client` has no design system. Optional React
137
142
  hooks in `@lunnoa/client/react`. Partners bring their own UI.
138
143
  7. **Reuse before create.** Before proposing any new entity type, table,
@@ -176,6 +181,7 @@ can execute. Use this shape:
176
181
  - [ ] `npx @lunnoa/client ai-connections list --json` before choosing an agent model
177
182
  - [ ] agents / workflows / entity-types / tables list — reuse before create
178
183
  - [ ] codegen — if building typed entity/workflow accessors
184
+ - [ ] `npx lunnoa-client status` before editing `lunnoa/`, then `pull` before `push`
179
185
 
180
186
  ### Resources (every row says REUSED or CREATED)
181
187
  | Kind | Slug / id | Reused / created | Notes |
@@ -188,7 +194,8 @@ can execute. Use this shape:
188
194
 
189
195
  ### Runtime paths
190
196
  - Ad-hoc: actions.run / runs.start / workflows.execute
191
- - Deploy: npx @lunnoa/client workflows|agents deploy …
197
+ - Sync: JSON in `lunnoa/`. `npx lunnoa-client status`, then pull, then push. Commit `lunnoa/lunnoa.lock.json`. No `--force` without a yes from the human.
198
+ - TypeScript deploy (generated, locked in the UI): npx lunnoa-client workflows|agents deploy …
192
199
  - Chat: agent stream-message / useAgentChat
193
200
 
194
201
  ### UI surfaces (headless)