@xanots/sdk 0.0.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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/agent-file-refresh-7XCACU34.js +95 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +184 -0
- package/dist/capture-HUV5BNTC.js +32 -0
- package/dist/chunk-26CNBZE2.js +963 -0
- package/dist/chunk-33ND6LKO.js +78 -0
- package/dist/chunk-3DUAGYGP.js +1550 -0
- package/dist/chunk-3IGNIP6R.js +89 -0
- package/dist/chunk-47WDWMBJ.js +14 -0
- package/dist/chunk-4BXJGVZ3.js +318 -0
- package/dist/chunk-4HT3BNZ7.js +675 -0
- package/dist/chunk-4YMD2OOZ.js +23 -0
- package/dist/chunk-5WGEURVI.js +156 -0
- package/dist/chunk-5YTBTF25.js +29 -0
- package/dist/chunk-6DHBYBTO.js +166 -0
- package/dist/chunk-76QBEIGO.js +66 -0
- package/dist/chunk-7DKX2SPN.js +100 -0
- package/dist/chunk-7REDODS2.js +468 -0
- package/dist/chunk-C56BC2FY.js +254 -0
- package/dist/chunk-DGSF2Q5H.js +60 -0
- package/dist/chunk-EMCAWMHC.js +3982 -0
- package/dist/chunk-EZG76F7R.js +159 -0
- package/dist/chunk-FJ6ZS5KB.js +3607 -0
- package/dist/chunk-GNPVYOPB.js +17 -0
- package/dist/chunk-HJPTWBLH.js +38 -0
- package/dist/chunk-IZW6MK3U.js +248 -0
- package/dist/chunk-LH6Z6KFV.js +247 -0
- package/dist/chunk-MZUI3JNX.js +93 -0
- package/dist/chunk-NDZFBZHC.js +167 -0
- package/dist/chunk-NOO35BQ2.js +37 -0
- package/dist/chunk-NUQCEOKA.js +13 -0
- package/dist/chunk-QTNO2WD6.js +56 -0
- package/dist/chunk-RVB7ITKN.js +49 -0
- package/dist/chunk-TRU5OHQ5.js +6904 -0
- package/dist/chunk-VKFQJEMW.js +814 -0
- package/dist/chunk-VTIL47DT.js +125 -0
- package/dist/chunk-WHOJWOSV.js +112 -0
- package/dist/chunk-WKP4D5MO.js +1692 -0
- package/dist/chunk-X4DVXBFY.js +38 -0
- package/dist/chunk-XQ22GLYS.js +118 -0
- package/dist/chunk-YDZYMZBE.js +670 -0
- package/dist/chunk-YGPIY3WT.js +1146 -0
- package/dist/chunk-YYRDLFSJ.js +602 -0
- package/dist/chunk-ZUTSMMAG.js +46 -0
- package/dist/cli.d.ts +331 -0
- package/dist/cli.js +33 -0
- package/dist/codegen-command-OOKK6AZK.js +40 -0
- package/dist/codegen.d.ts +114 -0
- package/dist/codegen.js +71 -0
- package/dist/completion-TIPPZN7H.js +298 -0
- package/dist/config-NL33PN4D.js +84 -0
- package/dist/deploy-command-4EQ7XX5G.js +451 -0
- package/dist/env-target-IFJ5QDHC.js +16 -0
- package/dist/ephemeral-command-3X3I4ULC.js +242 -0
- package/dist/index.d.ts +4477 -0
- package/dist/index.js +300 -0
- package/dist/init-command-R6H7ZXW5.js +27 -0
- package/dist/internal.d.ts +74 -0
- package/dist/internal.js +2065 -0
- package/dist/io-AMIKRLPC.js +11 -0
- package/dist/live-diff-Y3AKETHE.js +196 -0
- package/dist/lock-3CVKALKT.js +37 -0
- package/dist/lock-commands-XTR2YKLY.js +232 -0
- package/dist/login-command-ACJF6KWQ.js +167 -0
- package/dist/logout-command-MX3MJS5U.js +68 -0
- package/dist/loop-OM3P2Z6O.js +93 -0
- package/dist/marketplace-command-RATXTKXN.js +379 -0
- package/dist/meta-client-OW5WKWW7.js +170 -0
- package/dist/node.d.ts +59 -0
- package/dist/node.js +442 -0
- package/dist/profile-command-SWJ3SPKR.js +77 -0
- package/dist/release-command-VV5UVZKL.js +427 -0
- package/dist/response-CVAE2kMj.d.ts +1915 -0
- package/dist/routes-manifest-SP3ZXLMR.js +244 -0
- package/dist/runtime-V4C3AC3A.js +13 -0
- package/dist/sandbox-details-command-HJE5SPVG.js +18 -0
- package/dist/sandbox-export-command-QCJY4GMV.js +24 -0
- package/dist/static-host-D6KS7X45.js +138 -0
- package/dist/store-BQ97nKIF.d.ts +7760 -0
- package/dist/test-command-TSPBQHBM.js +383 -0
- package/dist/validate-command-4ERHJSCO.js +122 -0
- package/dist/verify-rollout-FAVQ6EJY.js +40 -0
- package/dist/workspace-K72NP7SX.js +49 -0
- package/dist/workspace-command-OF23DPNI.js +156 -0
- package/dist/workspace-export-AJMGN3CQ.js +10 -0
- package/llms/fields.md +104 -0
- package/llms/filters.md +183 -0
- package/llms/kinds-agent-mcp.md +11 -0
- package/llms/kinds-core.md +68 -0
- package/llms/kinds-realtime.md +76 -0
- package/llms/lambda.md +111 -0
- package/llms/legacy.md +43 -0
- package/llms/object-kinds.md +31 -0
- package/llms/statements-calls.md +23 -0
- package/llms/statements-catalog.md +309 -0
- package/llms/statements-data.md +58 -0
- package/llms/statements-runtime.md +48 -0
- package/llms/tests.md +58 -0
- package/llms/triggers.md +22 -0
- package/llms/values.md +29 -0
- package/llms-full.txt +1672 -0
- package/llms.txt +483 -0
- package/manifest.json +9592 -0
- package/package.json +110 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Realtime def shapes
|
|
2
|
+
|
|
3
|
+
> Read when the workspace defines a `realtimeServer()`, `realtimeChannel()`, or `realtimeMessage()` handler.
|
|
4
|
+
|
|
5
|
+
- **Realtime** — the only three-level chain: `realtimeServer` owns `realtimeChannel`s, which own `realtimeMessage` handlers. Pass the HANDLE, not a name (a channel path is unique only within its server).
|
|
6
|
+
- `realtimeServer({ name, guid?, description?, enabled?, canonical?, tags?, history? })` — the container.
|
|
7
|
+
- `enabled` defaults to **false** — the one `enabled` in the SDK that does.
|
|
8
|
+
- An enabled server with no active channel still refuses the handshake.
|
|
9
|
+
- `realtimeChannel({ name, server, guid?, description?, active?, input?, anonymousClients?, presence?, publish?, conversation?, delivery?, rateLimit?, tags?, history? })`
|
|
10
|
+
- `name` is a PATH (`"lobby"`, `"rooms/{room_id}"`); `input` types its `{param}` segments, NOT the payload. Every `{param}` MUST have a matching input, and that input must be a SCALAR and not a list (`json`/`object`/`array: true` have no URL form), or `realtimeChannel()` THROWS. `required: true` is NOT checked and is not needed — segment counts must match, so the segment is always present at join. Name charset as query (`A-Za-z0-9_-/{}`, max 200), and so is `tool`; `realtimeMessage` is NARROWER — no `/` or `{}`.
|
|
11
|
+
- Matching is STRICT: a literal segment beats a param (`rooms/lobby` and `rooms/{room_id}` coexist); segment counts must be EQUAL (`rooms/{room_id}` does NOT match `rooms/42/edit`); literals are CASE-SENSITIVE; an empty segment is REJECTED, not collapsed (a leading/trailing/doubled `/` matches nothing). `getChannel()` throws on an empty or slash-bearing param for that reason.
|
|
12
|
+
- An INACTIVE channel reports the same error as a nonexistent one — deactivating leaks nothing.
|
|
13
|
+
- `anonymousClients` is gated TWICE: the server admits the connection, then the channel admits the join. Setting it here alone is not enough.
|
|
14
|
+
- `publish?: { who?: "nobody"|"anyone"|"authenticated", direct? }` — `who` defaults to `nobody`: nobody can publish until you set it. `direct` (default false) lets a client address ANOTHER CLIENT via a frame's `options.socketId`, and is checked BEFORE `who`.
|
|
15
|
+
- `conversation?: { enabled?, limit?, ttl? }` — the client-visible TRANSCRIPT replayed to a joiner (distinct from `history`, which is execution history). ⚠ `limit` DEFAULTS TO 0 AND 0 MEANS OFF: `{ enabled: true }` alone records nothing and replays nothing, silently. `ttl` is an IDLE expiry of the WHOLE transcript, refreshed by every write (an active channel never ages out; a silent one loses all of it at once) — NOT a per-message age cap.
|
|
16
|
+
- `delivery?: { guarantee?: "at_most_once"|"at_least_once", perRecipient? }` — `perRecipient` is independent of the guarantee, is a NO-OP unless the channel declares a `deliver` trigger, and costs a stack PER RECIPIENT PER MESSAGE. Per-viewer redaction needs BOTH HALVES — this flag AND an active `deliver` trigger bound to the channel — and with either missing the payload is delivered UNCHANGED to everyone; `export()` warns on each half alone.
|
|
17
|
+
- `rateLimit?: { messagesPerMinute? }` — 0 = unlimited, checked BEFORE the handler runs. A COST guardrail, not a security control: an anonymous client is bucketed per CONNECTION (reconnecting resets it), and it fails OPEN when its store is down.
|
|
18
|
+
- `realtimeMessage({ name, channel, server?, guid?, description?, active?, auth?, deliverTo?, input?, middleware?, stack?, response?, responseShape?, history?, disabled?, tags? })` — the invocable unit (the realtime analogue of a query).
|
|
19
|
+
- `input` types the message PAYLOAD. `server` is required only when `channel` is a bare path.
|
|
20
|
+
- `deliverTo?`: `"channel"` (default) | `"sender"` | `"others"` | `"explicit"`. ⚠ `"explicit"` still delivers to NOBODY — nothing selects recipients from inside a handler, and `s.realtime.publish` (which originates an event INTO a channel) is not a substitute.
|
|
21
|
+
- Only `"channel"`/`"others"` fan out AND are written to the `conversation` transcript — a `"sender"` response is invisible to every future joiner.
|
|
22
|
+
- **Both input surfaces read as ordinary inputs:** `inp("body")` for a payload field, `inp("room_id")` for the channel's `{room_id}`. No session lookup, no frame parsing.
|
|
23
|
+
- A path param is bound ONCE at join and read from the connection thereafter, never from the frame — a sender cannot claim a room it did not join. The same values reach a channel `join`/`leave` trigger's stack.
|
|
24
|
+
- `s.realtime.get_session({ as })` — the CALLER's realtime session for the current frame. FLAT shape:
|
|
25
|
+
- `authenticated` bool · `client_id` text (the AUTHED ROW ID as text, `""` anonymous) · `dbo_id` int (the auth TABLE's id — NOT the user's row id; `0` anonymous — to look the caller up use `client_id`. `dbo_id` is an int in the same position and typechecks, so a gate that keys on it finds no user and refuses EVERYONE) · `socket_id` int (transport id) · `channel` text (resolved path, `""` in a server trigger) · `params` object (bound path params, `{}` when none — `ref("session.params.room_id")`) · `extras` object · `opened_at` decimal.
|
|
26
|
+
- Works in a realtime MESSAGE stack and in CHANNEL and SERVER trigger stacks; off that path it degrades to an anonymous session.
|
|
27
|
+
- For a path param prefer `inp("room_id")`. Reach for the session when you need the CONNECTION (identity/extras) — "who is this sender" on an anonymous-client channel.
|
|
28
|
+
- ⚠ THREE UNRELATED THINGS ARE CALLED A CLIENT ID: `session.client_id` (app-facing identity), `session.socket_id` (transport), and a frame's `options.client_id` (the at_least_once CURSOR handle). Conflating the first and last breaks at_least_once for anonymous clients.
|
|
29
|
+
- `s.realtime.publish({ server, channel, data, message?, authTable?, authId? })` — the PUSH direction: originate a server-authored event onto a channel from ANY stack, no client frame first.
|
|
30
|
+
- `server` is the handle or its NAME (resolved by name, not guid); `channel` is the FILLED-IN path (`channel.getChannel({ room_id: 42 })`), never the template — a constant still carrying `{param}` THROWS at author time, and a constant `server`/`channel` naming nothing this workspace registers WARNS at export.
|
|
31
|
+
- A PER-ROW path whose id is only known at runtime is built as a value, not with `getChannel()` (which needs the id at author time): `withFilters(c.text("rooms/"), fl.concat(ref("room.id")))`, or `s.set_var` + `s.text.prepend`. A computed `channel`/`server` — a `ref`/`inp`, or a constant carrying a filter chain — is left alone by the export check.
|
|
32
|
+
- DELIVERY-ONLY — fanned out as-is; does NOT invoke a `realtimeMessage()` handler even when `message` names one (a channel `deliver` trigger still runs).
|
|
33
|
+
- SERVER-AUTHORITATIVE — bypasses `publish.who`, which governs CLIENTS. Authorize in your own stack.
|
|
34
|
+
- ⚠ FAIL-SOFT — a missing/disabled server or dead bus is swallowed engine-side, so a mis-targeted publish is SILENT with no result to check.
|
|
35
|
+
- `authTable`/`authId` are ASSERTED attribution on the frame — not a credential, nothing validates them.
|
|
36
|
+
- **Client recipe (derive, never hardcode):**
|
|
37
|
+
- `server.getUrl(baseUrl)` → `wss://<host>/ws/<canonical>` — accepts the `https://…` instance base URL and normalizes the scheme. `channel.getChannel({ room_id: 42 })` → `"rooms/42"`, the path that goes in a frame's `channel` field. Both throw rather than guess. A canonical is minted by `xanots export <entry> --lock`.
|
|
38
|
+
- Auth is a bearer token passed as the websocket SUBPROTOCOL: `new WebSocket(url, token)`. No token = an anonymous client, admitted only where `anonymousClients: true`.
|
|
39
|
+
- Frames are JSON `{ action: "join"|"leave"|"broadcast"|"ack"|"ping"|"presence", channel, type?: <message name>, payload?, options?, id? }`. You must `join` before you may `broadcast`, and the server's context is ready only a moment after `open` — an immediate first frame is refused.
|
|
40
|
+
- `options` is `{ socketId?, client_id?, channel? }` — `socketId` addresses another client directly (needs `publish.direct`), `client_id` is the at-least-once cursor handle, and `options.channel` WINS over a top-level `channel`.
|
|
41
|
+
- ⚠ KEEP THE SOCKET ALIVE: an idle connection is REAPED after ~10 minutes. A LISTEN-ONLY client (a feed or dashboard that joins and rarely publishes) MUST send `{ action: "ping" }` (answered `pong`) or any frame periodically or it silently drops.
|
|
42
|
+
- Server frames: `join` (ack `{ joined: true, params }`, + `cursor`/`resumed` on at_least_once) · `message` · `replay` · `broadcast` · `presence_full`|`presence_join`|`presence_leave` · `conversation_start`|`conversation_end` (replayed frames flagged `conversation: true`) · `pong` · `ack` · `error`.
|
|
43
|
+
- ⚠ `broadcast` is a RECEIPT to the sender, not a delivery confirmation: `payload.delivered_local` counts recipients on the ANSWERING NODE ONLY, not the channel. It also carries `id` on at_least_once and `dropped: true` when the handler returned null.
|
|
44
|
+
- `error` carries `payload.message`, plus `code`/`limit`/`retry_after` when rate limited. `rate_limited` is the ONLY code — do NOT switch on `code`.
|
|
45
|
+
- An `error` is a per-frame refusal, NOT a disconnect — EXCEPT a failed handshake and a REFUSED `connect` trigger, which each send one and then CLOSE with code 4401.
|
|
46
|
+
- **Tenant instances (isolated DB):** a tenant's realtime objects live in the TENANT's database, so BOTH halves of a client must name the tenant.
|
|
47
|
+
- Socket: `server.getUrl(base, { tenant })` → `/ws/<tenant>:<canonical>`. ⚠ A bare canonical on a tenant host resolves against the INSTANCE workspace instead.
|
|
48
|
+
- That colon form is PECULIAR TO THE SOCKET. Every other tenant URL gives the tenant its OWN segment — the HTTP half of the same client is `https://<host>/tenant/<tenant>/api:<canonical>/…`. NO request header is required for either.
|
|
49
|
+
- Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `sandbox details` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.
|
|
50
|
+
- ⚠ `getUrl`/`socketUrl` are NOT idempotent — a `baseUrl` that already carries a `/ws/<…>` path (an earlier result of either) THROWS. Resolve ONCE from the http(s) base; pass that result to `new WebSocket`, never back in as a base.
|
|
51
|
+
- Still pass `{ tenant }` explicitly for a tenant on its OWN DOMAIN — the hostname carries it for HTTP, but there is nothing in the URL for the socket to lift.
|
|
52
|
+
- ⚠ Tokens are tenant-scoped (audience `<tenant>:<license>`, not the bare license), so one minted through the instance workspace is REJECTED by a tenant's realtime server — authenticate and dial through the same tenant.
|
|
53
|
+
- **Presence frames** (a `presence: true` channel only):
|
|
54
|
+
- `presence_full` carries `payload.members` — an ARRAY holding the WHOLE roster, including the receiving client. `presence_join`/`presence_leave` carry a single `payload.member`.
|
|
55
|
+
- A member is `{ id, dbo_id, authenticated, extras, joined_at }`: `id` the auth row id as a string (`""` anonymous), `dbo_id` the auth table's id (`0` anonymous), `extras` the connection's extras object, `joined_at` epoch SECONDS.
|
|
56
|
+
- Render from `presence_full`, then apply the deltas. The roster counts MEMBERS, not connections (refcounted per identity — a second tab fires no second `presence_join`).
|
|
57
|
+
- Join order: `join` ack → `presence_full` → (others get `presence_join`) → conversation replay → `replay` frames.
|
|
58
|
+
- A joined client can re-request the snapshot any time with `{ action: "presence", channel }`, answered to the SENDER only. A socket that never joined is REFUSED — the roster is not readable without membership.
|
|
59
|
+
- **Conversation frames — the transcript hydrates the client, so DO NOT build a hydration endpoint.**
|
|
60
|
+
- On a `conversation` channel the replay is PUSHED automatically at join, unasked: `conversation_start` (`payload.count`) → the last `limit` messages, each a normal `action: "message"` frame carrying its ORIGINAL `type` and `payload` plus `conversation: true` and the original `ts` → `conversation_end`.
|
|
61
|
+
- So the client needs NO fetch, no `GET /messages`, and no table read to paint the initial view. Render `message` frames identically either way; the backfill paints itself.
|
|
62
|
+
- ⚠ `{ enabled: true }` ALONE IS A NO-OP: `limit` defaults to 0, and 0 means RETAIN NONE (not retain everything), so the transcript is never written and never replayed, with no error. ALWAYS PASS `limit`.
|
|
63
|
+
- The POST-HANDLER broadcast payload IS the stored transcript row — a handler must broadcast everything the UI needs to render a past message (author name, id, `created_at`). Nothing else is replayed.
|
|
64
|
+
- Only `deliverTo` `"channel"`/`"others"` are RECORDED, so a `"sender"` response is invisible to every future joiner by construction.
|
|
65
|
+
- The transcript is a capped ring (`limit`, `ttl`), not storage. Persist to a table only for durability, search, or reads BEYOND that window — never merely to hydrate a joiner.
|
|
66
|
+
- **`delivery.guarantee: "at_least_once"` is a CLIENT CONTRACT, not just a channel setting.**
|
|
67
|
+
- The client must ACK what it receives — `{ action: "ack", channel, id }`, confirmed by `{ action: "ack", channel, payload: { cursor } }`.
|
|
68
|
+
- ⚠ An ANONYMOUS client must ALSO send a durable `options.client_id` in its JOIN frame (once; later acks need not repeat it). WITHOUT one it has no cursor, its acks are SILENTLY IGNORED, and it degrades to at_most_once. An AUTHENTICATED client is keyed by identity and needs no `client_id`.
|
|
69
|
+
- The missed gap arrives after join as `replay` frames, oldest-first, each with an `id` to ack.
|
|
70
|
+
- DISTINCT from the conversation transcript: `conversation_*` is the SHARED "what was said before I arrived", `replay` is the PER-CLIENT "what I missed while disconnected". Both may be on.
|
|
71
|
+
- How far back `replay` reaches is sized by `conversation.ttl` (here a REAL per-message age cut, and it BEATS `limit`), else `conversation.limit`, else 1000 — even on a channel with no transcript enabled.
|
|
72
|
+
- **What a message handler RETURNS decides delivery, and the failure directions are NOT symmetric.**
|
|
73
|
+
- A returned value fans out per `deliverTo` and becomes the transcript row.
|
|
74
|
+
- Returning NULL delivers NOTHING — the supported way to veto a message (the sender is told `dropped: true`).
|
|
75
|
+
- A payload REJECTED by the declared `input` also delivers nothing,; the detail goes ONLY to the sender.
|
|
76
|
+
- ⚠ But a handler that CRASHES FAILS OPEN: the sender's ORIGINAL, UNVALIDATED payload is broadcast to the channel unchanged. A handler doing redaction or authorization must NOT be the only thing between client input and subscribers.
|
package/llms/lambda.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Lambda bodies (JavaScript)
|
|
2
|
+
|
|
3
|
+
> Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`. Lambda workers are a bounded shared resource, each surface binds a different set of identifiers, and the hazards are not guessable.
|
|
4
|
+
|
|
5
|
+
**A lambda is an escape hatch, not a default.** The body runs outside the request's own
|
|
6
|
+
runtime, and a workspace has a BOUNDED pool of lambda workers every lambda in it shares
|
|
7
|
+
— so a call both crosses a process boundary and draws on a workspace-wide resource.
|
|
8
|
+
Reach for one only when the typed surface cannot express the work: if a native filter,
|
|
9
|
+
an `expr(...)`/`obj(...)` expression, or a plain statement can, use that. The crossing
|
|
10
|
+
is per CALL, not per element — an iterating filter sends the body ONCE and loops on the
|
|
11
|
+
other side, so one body over a whole list beats one called from inside a stack loop.
|
|
12
|
+
|
|
13
|
+
The lambda statement (`s.lambda({ as, code, timeout? })`) and eight filters run a
|
|
14
|
+
JavaScript body. **Write the body as a FUNCTION, not a `c.text` string** — the
|
|
15
|
+
bindings are its parameters, so the editor supplies them and a wrong name is a
|
|
16
|
+
compile error instead of a wrong value at runtime. Write it inline and the surface
|
|
17
|
+
is implied by where it sits; nothing names one:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
fl.map(({ $this }) => $this * 2) // map's bindings, typed from the position
|
|
21
|
+
fl.reduce({ initial_value: 0, code: ({ $result, $this }) => $result + $this })
|
|
22
|
+
s.lambda({ as: "total", code: ({ $var }) => $var.subtotal * 1.2 }) // ambient only — $this is a compile error
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The parameters are a fiction — only the BODY is sent, and the engine injects the
|
|
26
|
+
bindings as free identifiers — so DESTRUCTURE them. `(b) => b.$this` emits
|
|
27
|
+
`return b.$this`, and `b` is undefined at runtime (the SDK refuses it).
|
|
28
|
+
|
|
29
|
+
⚠ An inline `code:` arrow receives BINDINGS ONLY. `capture` is an option of
|
|
30
|
+
`lam.fn`, not a field of `s.lambda` or of a filter — to pass data in, move the
|
|
31
|
+
body into `lam.fn(fn, { capture })` (below). Writing `capture:` beside `code:`
|
|
32
|
+
is a type error, and the fix is to relocate the body, not to drop the field.
|
|
33
|
+
|
|
34
|
+
For a body built away from its call site:
|
|
35
|
+
|
|
36
|
+
- `lam.fn(({ $result, $this }) => $result + $this, { surface?, capture? })` — name a `surface` to check it here, or omit it and the call site checks it.
|
|
37
|
+
- `lam.raw("return 1", { surface })` — text, same validation.
|
|
38
|
+
- `lam.file("./lambdas/total.ts")` — a default-exported function in its own type-checked module, read as text at build time. The deterministic option under a bundler, where a function's source is whatever the bundler emitted. NODE ONLY, and it is the `lam` import that changes: `import { lam } from "@xanots/sdk/node"`. The isomorphic `lam` has no `file` (no filesystem in a browser bundle); its `fn` and `raw` are the same functions.
|
|
39
|
+
|
|
40
|
+
Nothing from the enclosing scope crosses implicitly. The body is sent as TEXT and runs
|
|
41
|
+
in a different process, so a closed-over `const rate` is undefined there, and the body
|
|
42
|
+
throwing on it returns the diagnostic text with HTTP 200 — a wrong VALUE, not an error.
|
|
43
|
+
Put what the body needs in `capture`; it arrives as the SECOND parameter and is emitted
|
|
44
|
+
ahead of the body as a `const` prelude:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
lam.fn(({ $this }, { capturedRate }) => $this * capturedRate, { surface: "map", capture: { capturedRate: rate } })
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
⚠ A capture key must NOT share its name with a module-scope binding. An inline body is
|
|
51
|
+
recovered with `toString()`, and a `.ts` loader renames one of two same-named bindings —
|
|
52
|
+
so the body reads `rate2` while the prelude declares `rate`, and `rate2` is undefined at
|
|
53
|
+
runtime (the body throws and the engine returns that text in the value slot with HTTP
|
|
54
|
+
200 — a wrong value, not an error). Build time refuses it. The key does not have to keep
|
|
55
|
+
the name of what it carries: `capture: { capturedRate: rate }` above is the safe form.
|
|
56
|
+
|
|
57
|
+
Capture JSON data only — string, number, boolean, null, object, array. A function,
|
|
58
|
+
`undefined`, `symbol` or `bigint` has no JSON form that survives, and a `Date`/`Map`/
|
|
59
|
+
`Set`/`RegExp` has one that LIES (a `Date` arrives as a string, the rest as `{}`), so
|
|
60
|
+
all of them are refused at build time — the class ones at any depth. Capture the plain
|
|
61
|
+
form and rebuild in the body (`d.getTime()` → `new Date(d)`). The captured type flows into
|
|
62
|
+
that second parameter (no explicit type arguments), and an object may be declared as an
|
|
63
|
+
`interface` or a `type` alias alike.
|
|
64
|
+
|
|
65
|
+
A body is a FUNCTION BODY: it must `return` its value. Bindings by surface — an
|
|
66
|
+
identifier outside its surface's set is undefined at runtime, and the SDK refuses
|
|
67
|
+
it at build time whichever spelling you use:
|
|
68
|
+
|
|
69
|
+
- every surface: `$env` · `$input` · `$var` · `$auth` (+ the `console` / `crypto` globals)
|
|
70
|
+
- `fl.lambda`: + `$this`
|
|
71
|
+
- `fl.map` · `fl.filter` · `fl.some` · `fl.every` · `fl.find` · `fl.findIndex`: + `$this` · `$index` · `$parent`
|
|
72
|
+
- `fl.reduce`: + `$this` · `$index` · `$parent` · `$result`
|
|
73
|
+
- `s.lambda`: ambient only — no `$this`, no `$parent`, no `$result`.
|
|
74
|
+
|
|
75
|
+
`$result` is `reduce`'s ACCUMULATOR (there is no `$acc`). `$this` is the element in
|
|
76
|
+
an iterating filter and the piped value in `fl.lambda`; `$parent` is the whole array
|
|
77
|
+
and exists only on the iterating filters. A stack variable is reached as
|
|
78
|
+
`$var.name` — it is NOT also injected as a bare `$name`.
|
|
79
|
+
|
|
80
|
+
Four hazards and the dependency route, all live-verified:
|
|
81
|
+
|
|
82
|
+
- ⚠ A body that THROWS does not fail the request: the engine returns its diagnostic
|
|
83
|
+
TEXT as the value with HTTP 200, so the failure reads as bad data. Validate before
|
|
84
|
+
consuming a lambda result numerically, and prefer a `lam.*` body, which cannot fail
|
|
85
|
+
this way for a binding reason.
|
|
86
|
+
- ⚠ `timeout` bounds a body that AWAITS, not one that spins. It is COOPERATIVE — only
|
|
87
|
+
observed at an `await` — so synchronous work runs to completion however long it takes:
|
|
88
|
+
a 1s `timeout` over a body that busy-loops for 3s lets it run all 3 and return
|
|
89
|
+
normally. Treat it as a bound on WAITING (a slow `fetch`), not a kill switch on
|
|
90
|
+
compute — if a loop could run away, bound it yourself inside the body.
|
|
91
|
+
- ⚠ A top-level `import`/`export` is a syntax error — the body is a function body, not
|
|
92
|
+
a module. Reach a dependency through the PRELOADED globals below, which need no
|
|
93
|
+
specifier. A dynamic `import("…")` or `require("…")` with a LITERAL specifier is not
|
|
94
|
+
portable: on an instance that bundles the body before running it, every literal
|
|
95
|
+
specifier is resolved ahead of time against a filesystem where none of them exist, so
|
|
96
|
+
`await import("node:crypto")` comes back as the TEXT `Could not resolve "node:crypto"`
|
|
97
|
+
with HTTP 200. Other instances resolve it at run time and it works — so it is
|
|
98
|
+
instance-dependent, and only the globals are not.
|
|
99
|
+
- Preloaded globals, live-probed — no specifier, so these work everywhere:
|
|
100
|
+
`_` · `aws4` · `axios` · `cryptojs` · `DateTime` · `ethers` · `fastXmlParser` ·
|
|
101
|
+
`jose` · `luxon` · `mailparser` · `math` · `moment` · `nodemailer` · `socks` ·
|
|
102
|
+
`uuid` · `utils`
|
|
103
|
+
…plus `fetch`, `Buffer`, `TextEncoder`/`TextDecoder`, and the `crypto` above
|
|
104
|
+
(`randomUUID`, `createHmac`, `subtle` all present). `Object.keys(globalThis)`
|
|
105
|
+
inside a body lists whatever else a given instance carries.
|
|
106
|
+
- ⚠ `console` output goes to the request LOG, not stdout. `log` · `error` · `warn` ·
|
|
107
|
+
`info` · `debug` · `trace` all route there; the body's `console` is a purpose-built
|
|
108
|
+
object, so anything outside that set is undefined and CALLING it throws — which,
|
|
109
|
+
per the first hazard, replaces the return value with the error text at HTTP 200.
|
|
110
|
+
|
|
111
|
+
TypeScript annotations survive in the body, and top-level `await` works.
|
package/llms/legacy.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Legacy paradigms and retired statements
|
|
2
|
+
|
|
3
|
+
> Read when the code was PULLED from an existing Xano instance rather than authored here — how a codegen'd tree reads and the three shapes in it that must not be "fixed", plus a `raw({ name: "mvp:…" })` statement, a `realtimeTrigger()`, or any name the catalogs do not list. None of the retired names are authorable for new code.
|
|
4
|
+
|
|
5
|
+
Older paradigms this SDK still supports and still emits when it decodes an existing
|
|
6
|
+
workspace. **Do not author these.** They are listed by name only so you recognize them
|
|
7
|
+
in pulled code rather than "fixing" them; each line names what to use instead.
|
|
8
|
+
|
|
9
|
+
Names overlap across the split deliberately — the engine reused words like
|
|
10
|
+
"realtime" and "channel" for both generations. A name matching is NOT evidence that
|
|
11
|
+
two things are the same object; check which list it came from.
|
|
12
|
+
|
|
13
|
+
- `c.expressionLegacy` — the older `const:expr` expression form, emitted by codegen for workspaces that still hold one — author `c.expression` instead.
|
|
14
|
+
- `realtimeTrigger()` — the SUPERSEDED realtime trigger, against the workspace-global realtime layer — a different object from the current `channel`, despite the similar name. For a join hook use `realtimeChannelTrigger({ actions: { join: true } })`; for message handling use a `realtimeMessage()` handler, which is the current equivalent of its `message` action (a message is an authored unit now, not a trigger action).
|
|
15
|
+
- `s.api.realtime_event` — publishes to the SUPERSEDED workspace-global realtime layer, NOT to a `realtimeChannel()` — its `channel` is a string against that layer, so pointing it at a current-layer channel path publishes into the void. Use `s.realtime.publish` instead: it names the owning `realtimeServer()`, so it addresses a real `realtimeChannel()`.
|
|
16
|
+
|
|
17
|
+
Retired statement VERSIONS — no `s.` surface exists. Pulled code shows them as
|
|
18
|
+
`raw({ name: "…" })` and they keep running as stored, so leave them; author the
|
|
19
|
+
replacement only for NEW code. Never swap one for the other — each version broke the last.
|
|
20
|
+
|
|
21
|
+
- `mvp:crypto_jwe_decode` → `mvp:crypto_jwe_decode2`
|
|
22
|
+
- `mvp:crypto_jwe_encode` → `mvp:crypto_jwe_encode3`
|
|
23
|
+
- `mvp:crypto_jwe_encode2` → `mvp:crypto_jwe_encode3`
|
|
24
|
+
- `mvp:crypto_jws_decode` → `mvp:crypto_jws_decode2`
|
|
25
|
+
- `mvp:crypto_jws_encode` → `mvp:crypto_jws_encode2`
|
|
26
|
+
- `mvp:connect_ncscale_send_log` — retired, no replacement
|
|
27
|
+
|
|
28
|
+
Statements the engine writes but will NOT import back — no `s.` surface exists, and
|
|
29
|
+
unlike the retired versions above these must be FIXED, not left alone. Pulled code shows
|
|
30
|
+
them as `raw({ name: "…" })`; `export()` refuses any bundle that still contains one.
|
|
31
|
+
|
|
32
|
+
- `mvp:placeholder` — an unconfigured statement slot the engine writes in place of a statement it could not resolve, so an export stays well-formed. There is no statement class behind it: importing a workspace that contains one fails outright with "Missing statement: mvp:placeholder". Replace it with the statement it stands in for before deploying — there is no destination where it runs.
|
|
33
|
+
|
|
34
|
+
## Reading a pulled tree
|
|
35
|
+
|
|
36
|
+
`codegen` emits objects as FACTORY calls (`table({...})`, `query({...})`, …) — the form
|
|
37
|
+
you author by hand — so inference survives the round trip.
|
|
38
|
+
Three shapes read differently and must not be "fixed": a trigger no factory expresses
|
|
39
|
+
(the realtime types, or a non-default `history`) stays `{...} satisfies TriggerDef`; a
|
|
40
|
+
statement the catalog cannot model round-trips verbatim through `raw()`
|
|
41
|
+
(`@xanots/sdk/codegen`); and an object ALREADY EMPTY upstream decodes to a def with
|
|
42
|
+
no `stack`, reported as `empty-source` — faithful, not a decode failure. ⚠ Workspace env
|
|
43
|
+
var VALUES ride inline in `xano/workspace.ts`, so treat a pulled tree as secret-bearing.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Object kinds
|
|
2
|
+
|
|
3
|
+
> Read when you need the full catalog of authorable primitives — which factory builds each, which register method takes it, and which payload key it lands under. Read it too when a `register*` call will not typecheck because the array was built by `.flatMap()`/`.concat()` across modules.
|
|
4
|
+
|
|
5
|
+
Author with the factory, register on the Xano instance, lands under the payload key. Each line ends with a one-liner on what the primitive is.
|
|
6
|
+
|
|
7
|
+
⚠ Composing a workspace from independently-authored modules: `register*` takes its defs however they arrive, but `modules.flatMap((m) => m.tables)` does NOT typecheck. `Array.prototype.flatMap` binds its element type to the FIRST element, so every later module's tables are checked against that one table's schema — the error compares two unrelated column names and mentions neither `flatMap` nor the cause. `.concat()` collapses the same way. Two spellings work: pass an array LITERAL (`registerTables([...a.tables, ...b.tables])` — spreads are fine, TypeScript infers the union across every element at once), or annotate the module array with the wide aliases `AnyTableDef` / `AnyQueryDef` / `AnyFunctionDef` / `AnyAddonDef`, exported for exactly the four def types whose generics can collapse this way. Widening the array costs nothing: the typing you need lives on the `table()`/`query()` handle you hold and pass to `s.db.*`, and is never read back off the registered array.
|
|
8
|
+
|
|
9
|
+
- function: `defineFunction` → `Xano.registerFunctions` → payload `function` — Reusable server-side logic (a custom function) callable from any stack via `s.function.run`.
|
|
10
|
+
- table: `table` → `Xano.registerTables` → payload `dbo` — A database table: typed columns (`f.*`), indexes, and views; the schema other kinds read and write.
|
|
11
|
+
- query: `query` → `Xano.registerQueries` → payload `query` — An HTTP API endpoint (verb + path) bound to an API group; the main request/response surface.
|
|
12
|
+
- api_group: `apiGroup` → `Xano.registerApiGroups` → payload `app` — A container that groups queries under a shared base path, CORS, and swagger config.
|
|
13
|
+
- trigger (database): `tableTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when rows change on a bound table (insert/update/delete/truncate). The changed row is exposed as `t.new`/`t.old`, typed to the table when a `table()` handle is bound. Config-only (no response). `search` filters rows in the DATABASE, so it uses `col("NEW.x")`/`col("OLD.x")`, not `t`; invalid with `truncate`, and insert/delete cannot read the absent side.
|
|
14
|
+
- trigger (realtime_server): `realtimeServerTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when a client connects to or disconnects from a realtime server; inspect the connecting client and its permissions via `t`. Bind with `realtimeServer`. Response-bearing.
|
|
15
|
+
- trigger (channel): `realtimeChannelTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when a client joins or leaves a channel; inspect the addressed channel path and the client via `t`. Bind with a `realtimeChannel()` handle (a bare path is ambiguous across servers). Response-bearing.
|
|
16
|
+
- trigger (toolset): `mcpServerTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when an MCP client connects to a bound MCP server; gate or annotate the exposed tools via `t.toolset`/`t.tools`. Response-bearing.
|
|
17
|
+
- trigger (toolset): `agentTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when a client connects to a bound agent; gate or annotate its toolset via `t.toolset`/`t.tools`. Response-bearing.
|
|
18
|
+
- trigger (workspace): `workspaceTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires on branch lifecycle events (branch new/merge/live); inspect the from/to branch and action via `t`. Config-only.
|
|
19
|
+
- trigger (error): `errorTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when an error signature is first seen, regresses, or is marked fixed; inspect the error, caller, statement, and occurrence counts via `t`. Config-only.
|
|
20
|
+
- tool: `tool` → `Xano.registerTools` → payload `tool` — An agent/MCP tool: a callable capability with typed inputs an AI agent can invoke.
|
|
21
|
+
- mcp_server: `mcpServer` → `Xano.registerMcpServers` → payload `toolset` — An MCP server exposing a set of tools to external MCP clients.
|
|
22
|
+
- agent: `agent` → `Xano.registerAgents` → payload `toolset` — An AI agent: an LLM configuration plus the tools it can call. Invoke it from any stack (query/function/task/tool/trigger) with `s.ai.agent.run` — no public endpoint; the result is a rich envelope whose completion text is at `.result`.
|
|
23
|
+
- task: `task` → `Xano.registerTasks` → payload `task` — A scheduled background job (cron/interval) that runs a stack on a timer.
|
|
24
|
+
- workflow_test: `workflowTest` → `Xano.registerWorkflowTests` → payload `workflow_test` — An end-to-end test: a named stack with NO input and NO response that invokes other objects (`s.function.call`, `s.task.call`, `s.api.call`) and asserts on what they bind with `s.expect.*`. `datasource` defaults to `""` (an EMPTY datasource, recommended); naming one makes the engine CLONE that datasource before every run, so pointing a test at production-sized data can be slow enough to fail the run — `"live"` warns at compile time.
|
|
25
|
+
- middleware: `middleware` → `Xano.registerMiddleware` → payload `middleware` — A reusable pre/post stack attached to a query/function/task/tool/API group to run before or after its own logic.
|
|
26
|
+
- addon: `addon` → `Xano.registerAddons` → payload `addon` — A reusable read fragment that enriches a query result by joining related table data.
|
|
27
|
+
- realtime_server: `realtimeServer` → `Xano.registerRealtimeServers` → payload `realtime_server` — A realtime (websocket) server: the canonical-addressed container that owns realtime channels. Off until `enabled: true`. Returns a handle with `getUrl(baseUrl)`/`getPath()` for the client's socket URL (`wss://<host>/ws/<canonical>`).
|
|
28
|
+
- channel: `realtimeChannel` → `Xano.registerRealtimeChannels` → payload `channel` — A realtime channel: a joinable path on a realtime server (`rooms/{room_id}`) with typed path params, join/publish policy, a client-visible conversation transcript, and delivery semantics. Owns message handlers. Returns a handle with `getChannel(params)` for the path a client joins.
|
|
29
|
+
- message: `realtimeMessage` → `Xano.registerRealtimeMessages` → payload `message` — A realtime message handler: a named message type on a channel with its own typed payload and stack — the realtime analogue of a query. Pass the `realtimeChannel()` handle as `channel` and the owning server comes with it.
|
|
30
|
+
- microservice: `microservice` → `Xano.registerMicroservices` → payload `microservice` — A container workload deployed alongside the workspace, called from a stack with `s.microservice.request`. Two mutually exclusive shapes via `kind`: `builtin` declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and `helm` points at a chart and its `values` — passing both throws. EARLY SURFACE, expected to change — every export of a workspace declaring one prints a notice saying so. `configs`/`volumes` are typed and `@deprecated` but NOT deployable: the engine rejects an import carrying either, so `export()` fails the build rather than letting the deploy fatal. Put a value the workload reads in a container's `env`, and storage in a container's own `volumes` (`emptyDir`/`persistent`/`config`). Container names are free-form — they need not match the microservice name, which is what a stack addresses. SECRETS RIDE ALONG — `chart.values` and `registryAuth.dockerconfigjson` are carried into the bundle, and into a pulled tree, verbatim (they must be, or a pulled microservice could not be redeployed). Both are stored strings with NO deploy-time indirection: `process.env.X` in the def resolves at EXPORT and writes the literal into the bundle, so it is not a way to keep the credential out. Either leave `registryAuth` unset (public image, or a credential attached outside this workspace) or treat the bundle and any pulled tree as secret material — keep them out of git, or rotate after. Export prints a notice per microservice carrying either field; `--strict` does not promote it. For a secret a STACK reads, the mapped surface is `workspaceConfig({ env })` + `env("NAME")`.
|
|
31
|
+
- workspace: `workspaceConfig` → `Xano.registerWorkspace` → payload `workspace` — Workspace-level configuration such as default middleware chains and request-history defaults per host kind.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Auth, cross-object calls, and microservices
|
|
2
|
+
|
|
3
|
+
> Read when the stack authenticates, calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
|
|
4
|
+
|
|
5
|
+
Auth & calls:
|
|
6
|
+
|
|
7
|
+
- `s.security.create_auth_token({ table, id, extras?, expiration?, as? })` — `extras` defaults to `{}`, `expiration` to `86400`s (`0` = never).
|
|
8
|
+
- `s.security.create_guid({ as? })` — bind a fresh GUID string. Takes nothing else.
|
|
9
|
+
- `s.function.run({ fn, input?, as?, runtime? })` / `s.function.call({ fn, input?, as? })` — run another function; `input` is keyed by the target's input names.
|
|
10
|
+
- `runtime?` runs it in the BACKGROUND: `{ mode: "async-shared" }` or `{ mode: "async-dedicated", cpu?, memory?, timeout?, maxRetry? }` (resources read at dedicated only). An async call DOES NOT return the result — it dispatches and continues, so `as` binds nothing; collect with `s.await({ ids })`. Omit for a normal call. Same block on `s.ai.agent.run`.
|
|
11
|
+
- `s.api.call({ api, input?, headers?, auth?, as? })` — invoke an endpoint. `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`'s — a `{ "Name": value }` record (values may be tagged), a `string[]` of `"Name: value"` lines, or one `Value`. `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING — a tagged `Value` deploys clean and then fails the run with `Param: token - Text filter requires an integer, float, string or boolean value`, since the engine stores that slot as plain text and never evaluates it. Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`) — elsewhere it deploys clean and 500s the first real request.
|
|
12
|
+
- `s.api.request({ url?, method?, params?, headers?, timeout?, follow_location?, verify_host?, verify_peer?, ca_certificate?, certificate?, certificate_pass?, private_key?, private_key_pass?, description?, output?, as? })` — external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`: `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH), `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter — the same record-of-values shape `response: { key: value }` takes); a tagged value NESTED inside an object or array THROWS at encode, so wrap a structured body in `obj({...})`, which encodes any depth as one `const:expr2` (→ query string for GET/HEAD/OPTIONS, body otherwise), `headers` a `{ "Name": value }` record whose values may be tagged (`{ "x-api-key": env("KEY") }`, each pair joined to a `"Name: value"` line) **or** a `string[]` of full header lines — prefer a header over a `?key=` query param for a credential — a URL travels into access logs, proxies and `Referer`. ⚠ Neither spelling is envelope-safe: the `as` envelope's `request` half mirrors `url`, `params` AND `headers`, so never return it raw from a credentialed request — read `response.result`. A NAME outside the header-token charset is refused, and a LITERAL value carrying a newline is refused; a value may hold `:` and spaces (`Bearer a: b` is a valid value). A TAGGED value cannot be checked at build time, so strip CRLF from caller-controlled input before this slot. Literal pairs lead the emitted array and computed ones follow, so the wire order is not the record's key order, `timeout` a `number` in seconds (1–86400), and `follow_location`/`verify_host`/`verify_peer` booleans. `description` (Settings tab) and `output` filters (Output tab) ride the envelope. SSL cert interdependencies (certificate↔private_key, ca_certificate→verify_peer) are checked at build time when statically provable, else by the engine at runtime. The `as` result is typed as the `{request, response}` envelope (`response.status: number`, `response.result: unknown`), so `InferResponse` resolves a `ref` to it. Same typed result on `webflow.request` and `microservice.request`.
|
|
13
|
+
- `s.stream.from_request({ url?, method?, …tls, as? })` — streaming external HTTP request (`mvp:streaming_api_request`); same typed field surface as `s.api.request` (no description/output envelope).
|
|
14
|
+
- `s.webflow.request({ path?, method?, …tls, as? })` — Webflow API request (`mvp:connect_webflow_api_request`); like `s.api.request` but addressed by `path` (host is engine-supplied).
|
|
15
|
+
- `s.task.call` / `s.tool.call` / `s.trigger.call` / `s.middleware.call` / `s.addon.call` — same `{ <target>, input?, as? }` shape against the named kind. `s.task.call` and `s.trigger.call` are WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`); `s.tool.call`, `s.middleware.call` and `s.addon.call` run from any stack.
|
|
16
|
+
- `s.action.call({ action, package?, input?, as? })` / `s.action.package.call(…)` — invoke a marketplace action; `package` is its slug.
|
|
17
|
+
- `s.cloud.job({ image?, command?, args?, secret?, template?, await?, as? })` — launch a containerized job. ⚠ `await` is SECONDS (default 60), not a boolean.
|
|
18
|
+
- `s.cloud.job.status({ id, as? })` · `s.cloud.job.await({ ids, timeout, as? })` — poll one job, or block on several (`ids` a list `Value`, `timeout` in seconds; both required).
|
|
19
|
+
|
|
20
|
+
Microservices (the `microservice()` def and the statement that calls it):
|
|
21
|
+
|
|
22
|
+
- `s.microservice.request({ host, path, port?, method?, params?, headers?, timeout?, follow_location?, as? })` — in-cluster microservice call (`mvp:microservice_request`); no TLS fields. ONLY `host`+`path` required; the rest default to the engine's values (`GET`/`{}`/`[]`/`10`/`true`), always emitted. Pass the `microservice()` DEF as `host` — it binds by NAME (how the engine resolves it), so a rename fixes every call site and the port is checked before deploy. `port?` folds into `host` as `"name:port"`: a def exposing ONE `servicePort` resolves automatically, SEVERAL requires it. A raw `"name:port"` string works, unvalidated, and is the only way to reach an instance-level microservice. `tenantDeploy: "manual"` on the def imports the row without starting the workload.
|
|
23
|
+
- `s.workflow_test.call({ workflowTest, datasource?, as? })` — run another workflow test from inside one, which is the only place it runs (see Gotchas in `llms.txt`). The odd one out: NO `input` (a workflow test takes none), and it carries `datasource?` instead — same clone caveat as the kind's own field.
|