You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/SPEC.md
+57-14Lines changed: 57 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1645,7 +1645,7 @@ Plan step for this as a standing rule.
1645
1645
|---|---|---|---|
1646
1646
|**Capture / Process / Apply**| Read structured state from a source, transform it, deliver it | Build (core domain) |`LOCKED`, §2 — built for clipboard/markdown |
1647
1647
|**Text injection** (a fixed hint/instruction pasted alongside a workflow's real output — e.g. telling an M365 Copilot chat what other tools are available) | Prepend or append configured static text to the payload | Build (core domain, `process-inject-text`, ADR-0006's self-registration pattern) — no templating engine; conditional injection composes for free with an upstream Decision node instead of adding branching logic to the node itself |`LOCKED`, built — `internal/domain/composition/processinjecttext.go`, e2e-verified (`composition-canvas-interactions.spec.ts`) end-to-end including via the generic ConfigField Inspector, no bespoke UI |
1648
-
|**Trigger**| Entry-point node: listen for *any* event source (hotkey, clipboard change, a browser-bridge DOM event per §5, an incoming MCP `tools/call` per §3.1, a schedule) and emit its data as the workflow's starting input — not "the hotkey mechanism," a general category the hotkey is one instance of. A trigger's output *is* the workflow's input; these are one concept, not two. | Each concrete event source adopts its own library behind an adapter (hotkey/schedule/filesystem-watch do; clipboard-watch is a small build); the abstraction unifying them into one node kind, and `TriggerService`'s registry/exclusivity, are Mill's own |`LOCKED`, built (manual/hotkey/schedule/clipboard-watch/filesystem-watch) — see §3.4 for the fuller map. DOM-event and MCP-call triggers remain unbuilt, gated on §5/§3.1 |
1648
+
|**Trigger**| Entry-point node: listen for *any* event source (hotkey, clipboard change, a browser-bridge DOM event per §5, an incoming MCP `tools/call` per §3.1, a schedule, Mill's own execution engine) and emit its data as the workflow's starting input — not "the hotkey mechanism," a general category the hotkey is one instance of. A trigger's output *is* the workflow's input; these are one concept, not two. | Each concrete event source adopts its own library behind an adapter (hotkey/schedule/filesystem-watch do; clipboard-watch is a small build); the abstraction unifying them into one node kind, and `TriggerService`'s registry/exclusivity, are Mill's own |`LOCKED`, built (manual/hotkey/schedule/clipboard-watch/filesystem-watch/callable/system-event) — see §3.4 for the fuller map, including `trigger-system-event`'s [ADR-0035](adr/0035-core-vs-composition-boundary.md) unparking. DOM-event and MCP-call triggers remain unbuilt, gated on §5/§3.1 |
1649
1649
|**Branch / routing** (UI-renamed from "Decision: route", ADR-0027) | Route execution down one of several named output edges based on a condition evaluated against the running payload | Node/graph semantics: build (core domain — composition rules). Expression evaluation underneath: adopt (`expr-lang/expr`, MIT, sandboxed/side-effect-free/loop-bounded by design — verified directly, not assumed) rather than hand-writing a condition parser |`LOCKED` (execution engine + authoring) — `internal/domain/composition`'s `ExecContext`/`ValidateGraph`/`nextNode` walk real branches end-to-end; `KindDecision` + `decision-route` NodeType render and connect on the canvas. Conditions are authored visually via a `react-querybuilder` rule builder (`DecisionEdgeInspector.tsx`), translated to `expr-lang/expr` — see §3.5's Branch row |
1650
1650
| **Decision (terminal outcome)** | Terminate a branch with a reusable, Configure-authored typed outcome: category + typed outputs + optional webhook; manual-review category parks into the Review queue first | Entity/CRUD/terminal-node semantics: build (core domain, the List/MCP-Server pattern). Webhook transport: reuse (the referenced HTTPRequest's own execution path via the extracted `httpsend.go` — never a second HTTP client). Park mechanism: reuse (the same `waitForApprovalFn` human-review uses) | `LOCKED`, built — [ADR-0027](adr/0027-decision-terminal-outcome.md): `internal/domain/decision`, `KindTerminal` + `decision-outcome` (no source handle, three-layer outgoing-edge rejection), Configure → Decisions tab, typed `outputBindings`, seeded branch-to-decision + manual-review examples proven against real DBOS, 96/96 e2e twice. Building it surfaced and fixed a real latent bug: the guardrail gate and dry-run tester read the *static* per-NodeType effect class, which would have hung a manual-review Decision run — generalized to `EffectForNode` (dynamic: a webhook-bearing Decision is `external`, a plain one `local`) and `NodeAlwaysParks` (human-review's hardcoded check, generalized). MCP write-tools for Decisions (`import_decision`/`export_decision`) are a named, mechanical follow-up — read Resources (`mill://decisions`) shipped |
1651
1651
|**Parallel Steps**| Fan out to multiple steps concurrently, then join | Graph/fan-in semantics: build. Concurrency execution: DBOS's `Queue`/`WithWorkerConcurrency` (§7) is a plausible real backing mechanism once designed, not hand-rolled goroutine management | ADR-0005 names it, deferred |
@@ -1809,10 +1809,12 @@ Zapier, Raycast — chosen because they're the platforms already anchoring
1809
1809
this design elsewhere in this doc) rather than invented from Mill's two
1810
1810
existing entry points (hotkey, manual click).
1811
1811
1812
-
**Built** — `KindTrigger`, five `NodeType`s, `TriggerService`, typed
1813
-
`ConfigField`s, hotkey exclusivity, and payload generation are all
1814
-
real code; the design reasoning below is accurate as originally
1815
-
written, not a later correction.
1812
+
**Built** — `KindTrigger`, six `NodeType`s (a sixth, `trigger-system-event`,
1813
+
added by [ADR-0035](adr/0035-core-vs-composition-boundary.md) — see the
|**Incoming MCP tool call**| C | An agent/chat client invokes one of Mill's exposed tools | Adopt (Go SDK's `Server.AddReceivingMiddleware`, already `LOCKED`, §3.1) |`OPEN` as a graph Trigger kind — validated as a real, established category (not a Mill invention) by n8n shipping its own dedicated MCP Server Trigger node |
1843
1845
|**Webhook / incoming HTTP**| C | External service POSTs an event to a Mill-owned endpoint | Not a library gap — Mill already runs an HTTP server in server-mode (Wails3 + stdlib `net/http`); the open question is purely whether Mill should run a public listener at all |`OPEN` — a scope/threat-model decision, not an adoption decision |
1844
1846
|**App/connector-specific** (e.g. email/IMAP) | B or C | Poll or push scoped to one external service | Depends on §4 Connectors |`PARKED` until §4 resolves — not a distinct Trigger *kind*, a connector-scoped instance of Group B/C |
1845
-
|**System/meta** (run failed, workflow updated) | D | Fired by Mill's own execution engine | Build, depends on §7 |`PARKED` until §7's execution engine lands — direct analog to n8n's Error Trigger / Workflow Trigger |
1847
+
| **System/meta** (decision-parked, run-completed/-failed/-cancelled) | D | Fired by Mill's own execution engine | Build (`trigger-system-event`, §7's engine) | `LOCKED`, built ([ADR-0035](adr/0035-core-vs-composition-boundary.md)) — direct analog to n8n's Error Trigger / Workflow Trigger, unparked once §7 landed. Config: `event` (options, one of the four above) + `workflowScope` (empty/"all", or one specific workflow's ID via the ADR-0009 picker, `RefKind: "workflow-scope"`). Fire payload (`InitialPayload`, JSON): `{event, runId, workflowId, workflowLabel, nodeId?, timestamp}` — `nodeId` only set for `decision-parked`. **Loop rule** (n8n's Error Trigger precedent, enforced at emission): a run whose OWN root trigger is `trigger-system-event` never emits a system event of its own, of ANY kind — a chain always bottoms out after one hop. Dispatch seam: `ExecutionService` exposes `SetSystemEventSink` (an injected-function seam, mirrors `SetConnectorLookup`); `TriggerService.DispatchSystemEvent` is wired in from `main.go`, keeping the import direction one-way (`executionsvc` never imports `triggersvc`). Emission sites: `parkForApproval` (decision-parked, `executionservice_guardrail.go`), `runWorkflow` (run-completed/run-failed, the one DBOS-registered function every run kind executes through), `CancelRun` (run-cancelled). First composed consumer: the seeded "Example: Forward pending approvals" workflow (§3.7's Update) — the forward-refactor proof. |
1846
1848
|**Callable by another workflow**| D | Fired only when a Child Workflow node (docs/adr/0010) invokes this workflow — never a real external event | Build (composition rule; execution rides on DBOS's native parent/child call, already adopted §7) |`LOCKED`, built — `trigger-callable` NodeType, no listener process (same shape as `trigger-manual`); direct analog to n8n's Execute Workflow Trigger |
1847
1849
1848
1850
**Architecture conclusion: each trigger type is its own `NodeType` under
@@ -2652,8 +2654,12 @@ Update has the full writeup. A menu-bar/dock *presence toggle* (hiding
2652
2654
the dock icon entirely) stays unbuilt — a different capability than the
2653
2655
badge — and **trigger-fire notifications remain a named future use of
2654
2656
the now-existing mechanism**, not built yet: the same `notify.SendPlain`/
2655
-
`SendActionable` primitives this pass added would carry it, once a
2656
-
concrete "fire on X" event is chosen.
2657
+
`SendActionable` primitives this pass added would carry it. **Update
2658
+
(ADR-0035): the concrete "fire on X" event this was blocked on now
2659
+
exists** — `trigger-system-event`'s four events — but wiring an
2660
+
OS-notification NodeType (rather than `NotifyPendingApproval` staying
2661
+
Settings-governed kernel chrome) is still unbuilt; the forward's own
2662
+
HTTP path is the first composed consumer, not this one.
2657
2663
2658
2664
**Attention escalation — `LOCKED` and built (docs/goals/archive/0023-
2659
2665
attention-escalation.md, ADR-0032's Update).** The `document.hasFocus()`-only
@@ -2671,12 +2677,28 @@ write or "Open in Mill" for a guardrail park. **(3)** alert-style
2671
2677
authorization is now actually requested (`notify.Start` previously
2672
2678
never called `RequestNotificationAuthorization` at all); Settings
2673
2679
documents the System Settings → Notifications → Mill → Alerts toggle.
2674
-
**(4)** a cross-device forward, `composition.SendJSONWebhook` (reuses
2675
-
the integration-http/decision-outcome transport tail) +
0 commit comments