@floless/app 0.5.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.
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: floless-app-bridge
3
+ description: This skill should be used when the user is driving the floless.app web UI (the local prompt-native front door at http://127.0.0.1:4317) and wants the terminal AI to act on what they did there — picking up queued Tweak requests, "use this template" requests, or the context of a node they selected/edited on the canvas. Use it when the user says things like "apply the tweak I just queued", "I clicked Tweak on the bom node — make that change", "pick up my floless request", "what's pending in floless", or after they double-click/Tweak a node in the UI. It reads the floless.app local API, applies the change to the app's .flo, recompiles, and clears the request.
4
+ metadata:
5
+ version: 0.1.0
6
+ ---
7
+
8
+ # floless.app ↔ terminal bridge
9
+
10
+ floless.app is the **thin web UI**; the **terminal AI (you) is the brain**. The UI cannot
11
+ edit `.flo` files or push into this terminal — instead it **queues requests** to a local API,
12
+ and you **pull** them here, do the real work (edit the `.flo`, recompile), and clear them. This
13
+ skill is that pull side. Pair it with the **`floless-app-workflows`** skill, which owns
14
+ the actual `.flo` authoring / install → compile → run loop.
15
+
16
+ ## The local API (http://127.0.0.1:4317)
17
+
18
+ The floless.app server runs locally on a **fixed port 4317** (override: `$PORT`). Confirm it's up
19
+ first: `curl -s http://127.0.0.1:4317/api/health` → `{"ok":true,"awareVersion":"…"}`. If it's not
20
+ running, the user starts it from the repo: `cd server && npm run dev` (or `npm run app`).
21
+
22
+ | Call | Purpose |
23
+ |---|---|
24
+ | `GET /api/requests` | List pending UI requests → `{ ok, requests: [...] }` |
25
+ | `DELETE /api/requests/:id` | **Clear** a request once you've handled it |
26
+ | `GET /api/app/:id` | The app's normalized topology + source + lock (selected-node context) |
27
+ | `POST /api/tweak` | (UI uses this to enqueue; you usually only read/clear) |
28
+
29
+ ### Request shapes (`/api/requests`)
30
+
31
+ ```jsonc
32
+ // A node Tweak (the ✎ Tweak button): change ONE node in plain English.
33
+ { "id": "uuid", "type": "tweak", "status": "pending",
34
+ "appId": "tekla-bom-by-phase", "nodeId": "bom",
35
+ "instruction": "also group by ASSEMBLY_POS and add a per-assembly subtotal" }
36
+
37
+ // A "use this template" request (a favorite dragged in / "Use in this app").
38
+ { "id": "uuid", "type": "use-template", "status": "pending",
39
+ "appId": "tekla-bom-by-phase", "templateId": "uuid",
40
+ "template": { "name": "…", "node": { "agent": "tekla", "command": "…", "config": {…} } } }
41
+ ```
42
+
43
+ The **selected-node context** you need is carried *in the request* (`appId` + `nodeId`), so you do
44
+ not need the UI's live selection — read that node's current source from the `.flo` to understand
45
+ what to change.
46
+
47
+ ## Workflow — pull, apply, clear
48
+
49
+ ```dot
50
+ digraph { "GET /api/requests" -> "for each pending" -> "read app .flo + node" ->
51
+ "apply change (edit .flo)" -> "recompile + reinstall" -> "DELETE /api/requests/:id" }
52
+ ```
53
+
54
+ 1. **Pull**: `curl -s http://127.0.0.1:4317/api/requests` → take the `pending` ones (oldest first).
55
+ Summarize them for the user before acting on anything destructive.
56
+ 2. **Locate the app source.** The editable `.flo` is the repo copy `demos/<appId>/<appId>.flo`
57
+ (preferred — it's version-controlled); the installed copy is `~/.aware/apps/<appId>/<appId>.flo`.
58
+ Edit the **repo** copy. `GET /api/app/<appId>` returns the parsed nodes + each node's
59
+ `config` (incl. the exec `code`) if you want the resolved view.
60
+ 3. **Apply the change** to the target `nodeId`:
61
+ - `type: tweak` → edit that node's `config.code` (exec C#) or `config.args` per the plain-English
62
+ `instruction`. Keep the node's contract (id, mode, edges) intact unless the instruction says
63
+ otherwise.
64
+ - `type: use-template` → add `template.node` as a new node and wire it in (see
65
+ `floless-app-workflows` for node/connection rules).
66
+ 4. **Recompile** so the UI's Run gate re-arms (it's gated on the lock's source-hash):
67
+ reinstall from the dir + `aware app compile <appId>` from `~/.aware/apps` (full loop +
68
+ gotchas live in `floless-app-workflows`).
69
+ 5. **Clear the request** so it leaves the UI's queue:
70
+ `curl -s -X DELETE http://127.0.0.1:4317/api/requests/<id>`. The UI's "requests" badge updates
71
+ live over SSE.
72
+ 6. **Verify** with a real run (the `verify`/Playwright standing rule) before reporting done.
73
+
74
+ ## Guardrails
75
+
76
+ - **You are the brain, the UI is not.** Never ask the UI to compose or mutate the `.flo`; it only
77
+ queues intent. All authoring happens here, through the `.flo` + `aware` CLI.
78
+ - Edit the **repo** `demos/<appId>/` source (tracked), then recompile into `~/.aware/apps/`.
79
+ - Always **clear** a request after handling it, so the user's "requests" count reflects reality.
80
+ - Don't invent node/command names — verify against the app's existing nodes (`GET /api/app/:id`).
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: floless-app-routines
3
+ description: This skill should be used when the user wants to set up a floless.app workflow (.flo app) to run automatically — "routines" — either on a timer (schedule) or on a live event (trigger). Triggers on requests like "schedule tekla-bom-by-phase every weekday at 7am", "run this workflow every 2 hours", "run my BOM whenever the Tekla model changes", "watch the model and run X on each change", "set up a trigger routine", "list my routines", "disable the morning routine", "delete that routine". Covers the floless.app routines REST API (create / list / edit / enable-disable / delete / run-now), the schedule presets, the event-trigger kind, the confirm-first rule, and the runnable gate.
4
+ metadata:
5
+ version: 0.1.0
6
+ ---
7
+
8
+ # Setting up floless.app routines (automatic .flo runs)
9
+
10
+ A **routine** runs an installed AWARE workflow (`.flo` app) automatically — the hands-off
11
+ equivalent of clicking ▶ Run workflow in floless.app. There are two **kinds**, both authored
12
+ through the same **`/api/routines`** REST surface:
13
+
14
+ - **`schedule`** — a **time-trigger**: the floless.app server's internal scheduler fires the
15
+ workflow on a clock (every weekday at 7am, every 2 hours, …).
16
+ - **`trigger`** — an **event-trigger**: a workflow whose source node is a *streaming*
17
+ `lifecycle:start` command (e.g. `tekla.watch`) runs once per emitted event. The server hosts a
18
+ long-lived `aware app run`; each change to the watched host (e.g. the Tekla model) fires the
19
+ chain. Only workflows that declare such a source are eligible.
20
+
21
+ The floless.app server is a thin localhost host on **port 4317**. Both kinds serialize host work
22
+ through the same run lock, and the server records/streams state. This skill drives that server so
23
+ the terminal AI can author routines on the user's behalf.
24
+
25
+ floless.app owns no engine: a routine is just a time- or event-trigger of `aware app run`. This
26
+ skill never composes a workflow — it sets up automation for one that already exists.
27
+
28
+ ## When to use
29
+
30
+ Use this skill when the user asks to **schedule / time / automate / trigger / watch-and-run** an
31
+ existing floless.app workflow, or to manage existing routines (list, edit, enable/disable, delete,
32
+ run now). Do **not** use it to build or edit a workflow itself — that is
33
+ `floless-app-workflows`.
34
+
35
+ **Which kind?** If the user names a clock ("at 7am", "every 2 hours", "daily") → **schedule**. If
36
+ they describe an event ("whenever the model changes", "on each change", "watch X and run") →
37
+ **trigger** — but a trigger only works if the workflow is trigger-eligible (see the trigger
38
+ workflow below); if it isn't, say so and offer a schedule instead. When unsure, ask.
39
+
40
+ ## The base URL + preflight
41
+
42
+ The server is at **`http://127.0.0.1:4317`** (the fixed floless.app port). Call it from the
43
+ terminal with `curl` (a non-browser caller sends no `Origin`, which the server's cross-origin
44
+ guard allows).
45
+
46
+ Before any routine call, confirm the server is up and licensed:
47
+
48
+ ```bash
49
+ curl -s http://127.0.0.1:4317/api/health
50
+ # → {"ok":true,"license":"valid",...}. If it doesn't respond, the floless.app desktop app
51
+ # isn't running — ask the user to start it. If license != valid/offline-grace, /api/routines
52
+ # returns 402; the user must sign in (the app surfaces a sign-in gate).
53
+ ```
54
+
55
+ ## The SCHEDULE workflow (always, in order)
56
+
57
+ 1. **Resolve + verify the workflow.** List installed apps and confirm the target exists:
58
+ `curl -s http://127.0.0.1:4317/api/apps`. Then confirm it is **runnable** (compiled, fresh
59
+ lock) — only runnable apps fire cleanly:
60
+ `curl -s http://127.0.0.1:4317/api/app/<id>` → check `app.runnable === true`. If it is not
61
+ runnable, tell the user it needs Compile first (don't schedule a broken app — the scheduler
62
+ would just record `skipped`).
63
+ 2. **Map the request to a structured schedule — never invent one.** The user says
64
+ "every weekday at 7am"; translate it to a `ScheduleSpec` (see below). If anything is ambiguous
65
+ (which days? what time? what inputs?), **ask before creating it.** Echo the parsed schedule and
66
+ inputs back in plain English ("Weekdays at 07:00, phase 3 — create this?") and wait for a yes.
67
+ 3. **Coerce inputs against the app's declared inputs.** Read `app.inputs` from
68
+ `/api/app/<id>`; pass only declared inputs, with the values the user gave (or omit one to use
69
+ the app's default). The server coerces values + drops unknown keys, but pass clean values.
70
+ 4. **Create it.** `POST /api/routines` with `{ name, workflow, schedule, inputs?, enabled? }` (see
71
+ the API reference). On success the routine appears live in the floless.app Routines panel (the
72
+ server broadcasts over SSE).
73
+ 5. **Confirm back to the user** what was scheduled and when it next fires (`routine.nextFireAt`).
74
+
75
+ ## The TRIGGER workflow (event-driven — always, in order)
76
+
77
+ 1. **Resolve + verify the workflow is trigger-eligible.** `curl -s http://127.0.0.1:4317/api/app/<id>`
78
+ and check **`app.triggerSource`** — it must be non-null (`{ nodeId, agent, command, inputs }`).
79
+ Null means the workflow has no streaming `lifecycle:start` source and **cannot** be a trigger
80
+ routine — tell the user, and offer a schedule instead. Also confirm `app.runnable === true`
81
+ (a not-runnable app starts `blocked` — needs Compile).
82
+ 2. **Confirm — never invent.** Echo back, in plain English, what will be watched and what runs
83
+ ("Run `tekla-bom-by-phase` whenever the Tekla model changes (tekla/watch) — set this up?") and
84
+ wait for a yes. **Source params are not settable in v1** (the watch's `filter` etc. live in the
85
+ workflow's node config, not as routine inputs) — don't promise to set them; if the user needs a
86
+ different filter, that's a workflow edit (`floless-app-workflows`), not a routine knob.
87
+ 3. **Coerce inputs** the same way as a schedule (only the app's declared top-level `inputs`). Many
88
+ watch apps declare none — then omit `inputs`.
89
+ 4. **Create it.** `POST /api/routines` with **`{ kind:"trigger", name, workflow, inputs?, enabled? }`**
90
+ — **no `schedule`**. The server validates eligibility (re-checks `app.triggerSource`), records the
91
+ source binding, and — if `enabled` — starts the live watcher session immediately.
92
+ 5. **Confirm back to the user.** A trigger routine has **no `nextFireAt`**; instead report its live
93
+ state from `routine.session` (`{ state, firedCount, lastEvent, error }`) — e.g. "Listening now;
94
+ it'll run on each change." Enable/disable is the control; **run-now does not apply** (the API
95
+ rejects it — see gotchas).
96
+
97
+ ## Schedule presets (structured, NOT cron)
98
+
99
+ Pick exactly one shape. Times are **local machine time**, `HH:MM` 24-hour. The finest granularity
100
+ is **hourly** by design (no sub-hour schedules).
101
+
102
+ | User intent | ScheduleSpec |
103
+ |---|---|
104
+ | "every 2 hours" | `{"kind":"hourly","everyHours":2}` (must divide 24: 1, 2, 3, 4, 6, 8, 12, 24) |
105
+ | "every day at 7am" | `{"kind":"daily","time":"07:00"}` |
106
+ | "every weekday at 7am" | `{"kind":"weekdays","time":"07:00"}` (Mon–Fri) |
107
+ | "Mondays and Thursdays at 7am" | `{"kind":"weekly","days":["mon","thu"],"time":"07:00"}` |
108
+ | anything a preset can't express | `{"kind":"cron","expr":"0 4 * * 1"}` (5-field; local time) |
109
+
110
+ Weekday tokens: `sun mon tue wed thu fri sat`.
111
+
112
+ **Cron** is the "Custom" escape hatch — a standard 5-field expression
113
+ (`min hour day-of-month month day-of-week`, e.g. `*/15 9-17 * * 1-5` = every 15 min,
114
+ 9am–5pm, weekdays). Prefer a preset when one fits (more readable); reach for cron only
115
+ when none does. Granularity is bounded by the ~1-minute scheduler tick.
116
+
117
+ ## Quick examples
118
+
119
+ ```bash
120
+ # Create a SCHEDULE: BOM every weekday at 07:00, phase 3
121
+ curl -s -X POST http://127.0.0.1:4317/api/routines -H 'content-type: application/json' -d '{
122
+ "name":"Morning BOM export","workflow":"tekla-bom-by-phase",
123
+ "schedule":{"kind":"weekdays","time":"07:00"},"inputs":{"phase":3}}'
124
+
125
+ # Create a TRIGGER: run a watch workflow on every model change (no schedule)
126
+ curl -s -X POST http://127.0.0.1:4317/api/routines -H 'content-type: application/json' -d '{
127
+ "kind":"trigger","name":"Watch model","workflow":"tekla-watch-smoke","enabled":true}'
128
+
129
+ # List (trigger routines carry a live `session`; schedule routines carry `nextFireAt`)
130
+ curl -s http://127.0.0.1:4317/api/routines
131
+
132
+ # Disable (or re-enable) — for a TRIGGER this STOPS / STARTS the live watcher
133
+ curl -s -X PATCH http://127.0.0.1:4317/api/routines/<id> -H 'content-type: application/json' -d '{"enabled":false}'
134
+
135
+ # Change a schedule's timing — PATCH only the fields that change (schedule kind only)
136
+ curl -s -X PATCH http://127.0.0.1:4317/api/routines/<id> -H 'content-type: application/json' -d '{"schedule":{"kind":"daily","time":"06:30"}}'
137
+
138
+ # Run now — SCHEDULE kind only (a trigger 400s: its control is enable/disable)
139
+ curl -s -X POST http://127.0.0.1:4317/api/routines/<id>/run
140
+
141
+ # Delete (a trigger's live watcher is stopped first)
142
+ curl -s -X DELETE http://127.0.0.1:4317/api/routines/<id>
143
+ ```
144
+
145
+ ## Rules + gotchas
146
+
147
+ - **Confirm before create.** Never invent a schedule, a trigger, or inputs; echo the parse and get
148
+ a yes. For a trigger, confirm the workflow is eligible (`app.triggerSource != null`) first.
149
+ - **Cap is 15** routines per install (both kinds share the cap). The 16th `POST` returns **409**
150
+ with a clear message — relay it and offer to delete one.
151
+ - **Workflow is fixed after create** (both kinds). `PATCH` cannot change a routine's workflow or its
152
+ `kind`. To switch workflows or convert schedule↔trigger, delete and recreate.
153
+ - **Run-now is schedule-only.** `POST /api/routines/:id/run` on a trigger returns **400**
154
+ ("a trigger routine runs on its event — enable/disable it instead"). Don't offer run-now for a
155
+ trigger; the control is the enable/disable toggle (start/stop the live watcher).
156
+ - **The server runs it, not Claude.** Once created, floless.app runs the routine locally — the
157
+ scheduler fires a schedule on its timer; a trigger's long-lived watcher runs as long as it's
158
+ enabled and the desktop app is up. A host-unavailable schedule fire records an honest `failed`
159
+ run; a trigger that can't start (uncompiled/drift, or a host-exclusivity conflict) reports
160
+ `session.state:"blocked"` with a reason. No AI interpretation of results in Phase 1.
161
+ - **Host exclusivity (trigger).** A host-backed watcher (e.g. Tekla) and a manual/scheduled host run
162
+ are mutually exclusive — one live model, one consumer. Starting one while the other is live is
163
+ refused with a clear reason (surfaced as `blocked`). Generic/cloud sources are unconstrained.
164
+ - **One seam, no drift.** This skill and the in-app Routines panel both write through
165
+ `/api/routines`, so validation, eligibility, and the cap apply identically.
166
+
167
+ See `references/routines-api.md` for the full request/response shapes, both `Routine` kinds, the
168
+ trigger `session` snapshot, run records, and error statuses.
@@ -0,0 +1,130 @@
1
+ # floless.app routines REST API
2
+
3
+ Base URL: `http://127.0.0.1:4317`. All bodies are JSON (`content-type: application/json`).
4
+ Every `/api/*` route requires a valid subscription (else `402`); the floless.app desktop app holds
5
+ the token. A non-browser caller (curl) sends no `Origin`, so the cross-origin guard allows it.
6
+
7
+ ## The `Routine` object
8
+
9
+ A routine is one of two **kinds**. `schedule` carries a `schedule` + `nextFireAt`; `trigger` carries
10
+ a `trigger` binding and, in `GET /api/routines`, a live `session` snapshot (no `schedule`/`nextFireAt`).
11
+
12
+ ```jsonc
13
+ // kind: "schedule" — a time-trigger
14
+ {
15
+ "id": "morning-bom-export", // slug, generated from the name (unique)
16
+ "name": "Morning BOM export",
17
+ "kind": "schedule", // "schedule" | "trigger" (defaults to schedule if omitted)
18
+ "workflow": "tekla-bom-by-phase", // installed app id (immutable after create)
19
+ "inputs": { "phase": 3 }, // coerced to the app's declared input types
20
+ "schedule": { "kind": "weekdays", "time": "07:00" },
21
+ "enabled": true,
22
+ "created": "2026-05-28T14:54:03.859Z",
23
+ "updated": "2026-05-28T14:54:03.859Z",
24
+ "nextFireAt": "2026-05-29T05:00:00.000Z", // computed (UTC ISO); null when disabled/broken/trigger
25
+ "lastRun": { "at": "...", "status": "ok", "runId": "...", "durationMs": 42551 },
26
+ "history": [ /* most-recent 20 RunRecords */ ],
27
+ "broken": { "reason": "workflow not installed" }, // present only when the app is gone
28
+ "onResult": null // PHASE-2 reserved (AI handoff) — ignore in Phase 1
29
+ }
30
+
31
+ // kind: "trigger" — an event-trigger (streaming lifecycle:start source)
32
+ {
33
+ "id": "watch-model",
34
+ "name": "Watch model",
35
+ "kind": "trigger",
36
+ "workflow": "tekla-watch-smoke",
37
+ "inputs": {}, // app's declared top-level inputs only (often none)
38
+ "trigger": { "nodeId": "watch", "agent": "tekla", "command": "watch" }, // the streaming source
39
+ "enabled": true,
40
+ "created": "…", "updated": "…",
41
+ "nextFireAt": null, // always null for triggers (no clock)
42
+ "session": { // LIVE state — present in GET /api/routines only
43
+ "state": "listening", // "listening" | "blocked" | "error" | "stopped"
44
+ "firedCount": 5, // events seen this session
45
+ "lastEvent": { "at": "ISO", "summary": "Beam modified · B/4" }, // newest fire, or null
46
+ "error": null // stderr tail when state==="error"/"blocked", else null
47
+ },
48
+ "lastRun": null, "history": [],
49
+ "broken": { "reason": "workflow not installed" }, // app gone / no longer trigger-eligible
50
+ "onResult": null
51
+ }
52
+ ```
53
+
54
+ ### Trigger `session` states (live, server-pushed)
55
+
56
+ - **`listening`** — the watcher process is up and watching; the healthy steady state. Each event
57
+ increments `firedCount` and updates `lastEvent`.
58
+ - **`blocked`** — couldn't start: app not runnable (needs Compile / drift), or a host-exclusivity
59
+ refusal (a Tekla watcher and a manual/scheduled Tekla run can't both be live). `error` carries why.
60
+ - **`error`** — the watcher exited non-zero; `error` carries the stderr tail. Re-enable to retry
61
+ (no auto-restart, to avoid flapping).
62
+ - **`stopped`** — not running (disabled, or a finite stream ended). Enable to (re)start.
63
+
64
+ ### `ScheduleSpec` (structured presets, local machine time)
65
+
66
+ ```ts
67
+ { kind: "hourly", everyHours: number } // must evenly divide 24: 1,2,3,4,6,8,12,24
68
+ { kind: "daily", time: "HH:MM" }
69
+ { kind: "weekdays", time: "HH:MM" } // Mon–Fri
70
+ { kind: "weekly", days: Weekday[], time: "HH:MM" } // Weekday = sun|mon|tue|wed|thu|fri|sat
71
+ { kind: "cron", expr: string } // standard 5-field: min hour dom month dow
72
+ ```
73
+
74
+ `cron` is the "Custom" power-user variant (e.g. `"0 4 * * 1"` = Mondays 04:00,
75
+ `"*/15 9-17 * * 1-5"` = every 15 min 9–5 on weekdays). Local time; granularity is
76
+ bounded by the ~1-minute scheduler tick. Supports `* , - /` and lists.
77
+
78
+ ### `RunRecord`
79
+
80
+ ```jsonc
81
+ { "at": "ISO", "status": "ok" | "failed" | "skipped", "reason": "…?", "runId": "…?", "durationMs": 0 }
82
+ ```
83
+ - `ok` — the run completed and no node reported failure.
84
+ - `failed` — `aware app run` errored, or a node returned `{ ok:false }` (e.g. "No live Tekla model").
85
+ - `skipped` — the app was not runnable at fire time (needs Compile / drift).
86
+
87
+ ## Endpoints
88
+
89
+ | Method | Path | Body | Result |
90
+ |---|---|---|---|
91
+ | GET | `/api/routines` | — | `{ ok, routines: Routine[], max: 15 }` (trigger routines include a live `session`) |
92
+ | POST | `/api/routines` | schedule: `{ name, workflow, schedule, inputs?, enabled? }` · trigger: `{ kind:"trigger", name, workflow, inputs?, enabled? }` | `201 { ok, routine }` · `400` invalid/ineligible · `409` at cap |
93
+ | PATCH | `/api/routines/:id` | any of `{ name, schedule, inputs, enabled }` (a trigger ignores `schedule`; `enabled` start/stops its watcher) | `{ ok, routine }` · `404` · `400` |
94
+ | DELETE | `/api/routines/:id` | — | `{ ok }` · `404` (stops the watcher first for a trigger) |
95
+ | POST | `/api/routines/:id/run` | — | `{ ok }` (schedule: enqueues a real run now) · `400` for a **trigger** · `404` |
96
+
97
+ Supporting reads (same server):
98
+ - `GET /api/health` → `{ ok, license, ... }` — preflight.
99
+ - `GET /api/apps` → `{ ok, apps: [{ id, nodes, layout, provider }] }` — does the workflow exist?
100
+ - `GET /api/app/:id` → `{ ok, app }` — `app.runnable` (compiled+fresh) and `app.inputs`
101
+ (`[{ name, type, default, description }]`) to seed/validate routine inputs, **plus
102
+ `app.triggerSource`** (`{ nodeId, agent, command, inputs } | null`) — non-null = trigger-eligible.
103
+
104
+ ## Error statuses
105
+
106
+ - `400` — validation: missing name, unknown/invalid workflow id, malformed schedule
107
+ (bad `HH:MM`, empty weekly `days`, `everyHours` not a divisor of 24); a `kind:"trigger"` create
108
+ on a workflow that is **not trigger-eligible** (`{ "error": "workflow is not trigger-eligible …" }`);
109
+ or `POST /:id/run` on a **trigger** (`{ "error": "a trigger routine runs on its event — enable/disable it instead" }`).
110
+ - `402` — no valid subscription (the app must be signed in).
111
+ - `404` — no routine with that id.
112
+ - `409` — `{ "error": "Routine limit reached (15). Delete one first." }` — the cap (both kinds share
113
+ it). Relay it and offer to delete an existing routine.
114
+
115
+ ## Notes on behavior (so expectations are right)
116
+
117
+ - **Local time, DST-safe.** Schedules are wall-clock local; 07:00 stays 07:00 across a DST change.
118
+ - **Missed fires skip forward.** If the server was down at fire time, the next fire is the next
119
+ future occurrence (no catch-up burst on boot).
120
+ - **Serialized.** Scheduled runs share the single active-run lock with manual ▶ Run; a fire that
121
+ coincides with another run waits its turn (the host bridge does one exec at a time).
122
+ - **Inputs.** Only the app's declared inputs are kept, coerced to their declared type; omit an
123
+ input to use the app's default. Keys are validated `[A-Za-z0-9._-]`.
124
+ - **Triggers (event-driven).** A `kind:"trigger"` routine hosts a long-lived `aware app run` of a
125
+ streaming `lifecycle:start` source (outside the schedule run-lock); `enabled` start/stops that
126
+ watcher. State is **ephemeral** — `session` is recomputed live (server restart re-derives it) and
127
+ pushed over the `trigger-session-changed` SSE event. **v1 sets no source params**: the watch's
128
+ `filter` etc. live in the workflow's node config, not as routine inputs — to change them, edit the
129
+ workflow (`floless-app-workflows`), not the routine. A host-backed watcher is mutually
130
+ exclusive with manual/scheduled host runs (one live model = one consumer).