13. Events, Interfaces, and Complete Apps
A Flow can be correct and still be unusable. Until somebody or something has a supported way to start it, it is an internal implementation.
This chapter turns the incident logic from the preceding chapters into an Incident Desk App. The same typed decision becomes available to an App user, a scheduled run, and an authenticated REST caller. We will also give the App a real interface—not a screenshot—and keep every boundary explicit enough to govern, version, and trace.
The result is not one Event with several decorative labels. Different surfaces have different setup contracts. They share the business function while using adapters suited to their callers.
Release check: An event node is a triggerable entry inside a Flow. An App Event binds a compatible entry to a surface, configuration, variable overrides, execution location, and Flow version. Quick Action and Cron can target a Simple Event. A remote REST App Event instead targets a Simple setup entry that registers one or more Generic handler entries. Page actions identify a Board event node and execute it with the current interface state. App roles govern ordinary Event execution and editing; there is no separate per-Event ACL. An Event is Local or Remote—never Hybrid—even when its Board is Hybrid. Numbered Flow versions work today; weighted canary dispatch is represented in the data model but is not wired into the audited execution paths. Remote REST OpenAPI, edge type validation, and rejected-run evidence also have current limitations described below.
13.1 An event node is an entry; an App Event is an exposure
Section titled “13.1 An event node is an entry; an App Event is an exposure”The word Event appears at two levels because the platform needs two distinct objects.
An event node lives on the Board. It begins execution and defines what the Flow receives. A Simple Event has no data contract of its own. A Generic Event carries a payload and can expose additional typed output pins. Chat and Mail Events provide contracts specialized for those interfaces.
An App Event lives around the Flow. It selects:
- the Flow, event node, and either Latest or a numbered Flow version;
- an interface or sink such as Quick Action, REST, Cron, Chat, or Deeplink;
- Local or Remote execution where that surface offers a choice;
- activation, route, exposure, and surface-specific settings;
- variable overrides, including protected secret values; and
- operational release information belonging to that exposed entry.
That separation is useful. One event node may be exposed by several compatible App Events with different schedules, settings, or variable overrides. Conversely, changing a REST route should not require inserting route strings into the business logic.
The memorable formulation is:
The event node says what can begin. The App Event says how this world may begin it.
Compatibility still matters. An App Event cannot wrap every event node in every surface. The current product offers Generic Form, API, and Deeplink setups for a Generic Event; Quick Action, API, Cron, Daemon, Deeplink, REST, and MCP setups for a Simple Event; and specialized interfaces for Chat and Mail. The setup screen filters the choices rather than pretending incompatible contracts can be connected.
13.2 Keep one decision and write honest adapters
Section titled “13.2 Keep one decision and write honest adapters”Our shared decision has a deliberately small result:
interface IncidentResult { severity: string team: string runbook: string}
function decideIncident( systemId: string, report: string, impact: string): (result: IncidentResult) { // Deterministic classification and system-directory lookup}This Function owns the rule. The interfaces do not copy it. They adapt their input into its three parameters and carry its structured result back to their caller.
The Quick Action adapter is a Simple Event. Its fields come from exposed Board variables:
eventsSimple triageQuickAction() { const result = decideIncident(systemId, report, impact) info({ message: `Route ${systemId} to ${result.team}; open ${result.runbook}`, toast: false })}A Cron App Event can target that same Simple Event. There is no interactive form during a scheduled run, so the scheduled App Event supplies the relevant variable overrides instead. The entry shape is identical even though the caller is not.
REST needs another adapter:
eventsGeneric triageRest( payload: Struct, systemId: string, report: string, impact: string, _client: Struct) { const result = decideIncident(systemId, report, impact) return result}Each named parameter becomes an output pin on the Generic Event entry. The inbound dispatcher
fills those pins from the request. return publishes the Event result, which the REST surface can
turn into a response.
The _client parameter is not a user-supplied business field. It carries trusted metadata produced
by the selected authentication path, such as verified OAuth claims or a connected-App identity.
The Flow may inspect that context and make a finer authorization decision of its own.
Why not register decideIncident directly? A Function is a callable layer, not an independently
triggerable graph entry. REST registration stores the ID of a concrete event/handler node that the
runtime can invoke. triageRest is the thin, visible adapter between those two contracts.
| Surface | Board entry | Where input comes from |
|---|---|---|
| A2UI button | Selected Simple or Generic Event | Current Page element state and invocation context |
| Quick Action | Simple Event | Exposed variables shown as a form |
| Cron | Simple Event | App Event configuration and variable overrides |
| REST route | Generic handler registered by a Simple setup Event | HTTP body, query, headers, and authenticated client metadata |
Shared logic belongs in the Function. Surface behavior belongs in the adapter. That small amount of explicit repetition is useful because it keeps a human reviewer from confusing an interactive user action with an unattended schedule or a public network boundary.
13.3 A Page should be experienced, not merely described
Section titled “13.3 A Page should be experienced, not merely described”The /triage experience asks for an affected system, a report, and its business impact. A button
then invokes the Page’s incident Event and renders its structured result.
The interface below is a real, hydrated React component embedded in this book. Change its values and press Triage incident. Its calculation is an explicitly labelled local mock: no Flow-Like runtime or external endpoint is called from the book.
Embedded React prototype
Incident Desk
Route an interruption to the people and runbook that can resolve it.
- Responsible team
- payments-on-call
- Runbook
runbooks/payments.md
workflow_event → triageQuickActionExample output is ready. Change the form to preview another response.
The production Flow-Like Page uses A2UI rather than this book component. Its component tree would contain typed text fields, a select, a button, and result cards. Components whose current values matter to execution are marked as event-relevant. The button’s action identifies the event node:
{ "name": "workflow_event", "context": { "nodeId": "<triage-page-event-node-id>" }}The action context is for routing identity, not an improvised form-data envelope. At invocation, the client combines the current Page elements and relevant input values into the execution payload. The Flow can read the current element values through the A2UI catalog nodes. This keeps the Page model and its behavior connected without hiding a JavaScript callback inside the button.
A Page does not own its navigation path. A UI App Event points to the Page and receives a route
such as /triage. Today that App interface is an authenticated experience. Anonymous Page hosting
is a planned surface, not a guarantee this chapter relies on.
13.4 Quick Action and Cron share a shape, not an identity
Section titled “13.4 Quick Action and Cron share a shape, not an identity”The Quick Action can expose the three ordinary variables declared in the fixture:
let systemId = "payments"let report = "Production is on hold"let impact = "production-stopped"An App user sees those values as fields, changes them, and invokes triageQuickAction. A builder
can create a second App Event on the same entry for a periodic verification job. That Cron Event
can overwrite systemId, report, or impact without editing the Board.
The effective value order is:
- a permitted invocation/runtime override;
- the App Event’s variable override; and
- the Board default.
The people who may write the Flow define which variables participate in that configuration contract. A secret variable is handled separately and omitted from ordinary read responses. The REST API key in this chapter has no source value:
@description("API key configured on the REST App Event")@secretconst incidentApiKey: stringIts App Event supplies the protected value. The caller’s API key proves permission to cross the REST boundary; it is not automatically a credential that nodes should forward to another system. Those are separate trust decisions.
13.5 A REST App Event is a setup program
Section titled “13.5 A REST App Event is a setup program”A single API Event can expose one configured endpoint. The REST surface is more capable and therefore more explicit: the Flow builds a server configuration, registers handlers, attaches authentication, publishes OpenAPI routes, and reaches the REST Server node.
The canonical fixture performs that setup as follows:
eventsSimple configureIncidentRest() { const base = serverConfig({ host: "127.0.0.1", port: 0, tls: { secure: false } }) const routed = base.registerFunction({ path: "/triage", method: "POST", fnRefs: [triageRest] }) const secured = routed.registerAuth({ auth: apiKey({ header: "x-api-key", key: incidentApiKey }) }) const documented = secured.registerOpenApi({ path: "/openapi.json", uiPath: "/docs" }) const address = server({ config: documented }) address { onListening: { info({ message: "Incident REST surface registered", toast: false }) } onClose: { info({ message: "Incident REST surface closed", toast: false }) } execError: { warn({ message: "Incident REST setup failed", toast: false }) } }}fnRefs: [triageRest] is reference metadata, not an array passed to an ordinary pin. It tells
Register REST Function which triggerable handler to publish. For the current remote path, register
one handler per route; the remote setup persists the first reference, while the local socket
implementation can invoke several.
Create the App Event with these settings:
- Target
configureIncidentRest. - Select REST, Remote, and the intended Public or Internal exposure.
- Pin a tested numbered Flow version.
- Configure the
incidentApiKeysecret override. - Choose a stable alias such as
incident-desk. - Save and inspect the setup status and registered routes.
In a remote runtime, REST Server does not bind a long-lived socket inside the setup run. It emits the composed server configuration to the platform. Saving the App Event runs that setup, validates that it reached REST Server, and persists the resulting route and authentication registrations.
That makes setup part of the release. A newly created REST Event is rolled back when its first setup fails. When an update fails, inbound traffic continues to use the last successful setup version while the error remains visible to the builder. A broken draft does not silently replace a working route.
After a successful setup, the public paths have this shape:
POST /r/incident-desk/triageGET /r/incident-desk/openapi.jsonGET /r/incident-desk/docs13.6 The HTTP boundary is structured—and still evolving
Section titled “13.6 The HTTP boundary is structured—and still evolving”Call the route with an API key and JSON body:
POST /r/incident-desk/triagex-api-key: <configured secret>content-type: application/json
{ "systemId": "payments", "report": "Production is on hold", "impact": "production-stopped"}The handler returns one object:
{ "severity": "SEV-1", "team": "payments-on-call", "runbook": "runbooks/payments.md"}A plain returned value becomes a 200 application/json response. Advanced handlers can return a
response envelope containing status, headers, content type, and body when the HTTP contract needs
more control.
For named handler parameters, query values are collected first and JSON object fields overwrite a
same-named query value. The runtime also makes the request, method, path, query, headers, raw body,
and _client metadata available through reserved handler parameters. Keep the public contract
small; accepting the entire request object by default only makes future compatibility harder.
Flow-Like can publish OpenAPI JSON and an interactive browser UI for these registrations. The current remote document reliably describes routes, methods, and authentication, but its request and response bodies remain open objects. The local socket implementation has richer pin-derived request schemas. Neither remote document currently proves that the inbound router enforces the declared Flow types before execution.
That leads to an important separation between design doctrine and the current release:
Doctrine: Reject a request at the earliest boundary where its incompatibility is known, and record the rejection as execution evidence.
Today malformed JSON is rejected before dispatch. Authentication failure and unmatched routes are also rejected at the edge. But field values are not yet validated against the handler’s pin schema there; a mismatch may fail later when a node evaluates its typed pin.
Pre-dispatch rejections also do not currently appear as rejected Runs. The persisted Run status model has Pending, Running, Completed, Failed, Cancelled, and Timeout, but no Rejected state. Invalid JSON, failed authentication, and an unmatched route can therefore return before a Run row exists. A type error that reaches execution can appear as a failed Run. The intended design is stronger: create attributable rejection evidence without pretending rejected work executed.
13.7 Identity, permission, and credentials are different boundaries
Section titled “13.7 Identity, permission, and credentials are different boundaries”An ordinary App member needs the App-level Execute Events permission to invoke an Event. A builder needs Write Events to create, activate, reconfigure, or repoint one. Owners and admins inherit these abilities, and a custom role may receive them. There is currently no separate ACL on each App Event.
The Flow can still make a domain decision after entry. User-context nodes expose the executing subject, role, permissions, and role attributes, so the graph can ask questions such as “may this caller triage payment incidents?” That check is application logic and remains visually reviewable. The core context also models arbitrary custom attributes, although the normal audited API path does not yet populate all of them.
REST adds another boundary:
| Concern | What answers it |
|---|---|
| May this App member invoke ordinary Events? | App role and ExecuteEvents |
| May this network caller enter the REST route? | Configured REST authentication |
| Which connected App called an Internal route? | Connected-App proxy identity and role |
| May this caller perform the domain action? | Explicit checks inside the Flow |
| Which credentials may nodes use downstream? | Caller or App Event/sink credentials, chosen for that execution setup |
Public REST supports no authentication, API key, static bearer token, Basic authentication, HMAC-SHA256, and OAuth/OIDC bearer validation. This exercise deliberately uses an API key. Leaving Register REST Auth disconnected creates an unauthenticated surface and should be an explicit, reviewed decision—not an accidental default.
API-key possession does not identify a person. Verified OAuth claims can contribute subject,
issuer, audience, scopes, and related metadata to _client. An Internal REST Event is reachable
through an approved App connection rather than the public router, but that connection does not
bypass authentication configured on the REST registration.
Interactive invocations can execute with the signed-in caller’s credentials where configured. Unattended and public Event sinks normally use credentials deliberately stored with the App Event or sink. Platform storage credentials are scoped separately. Treating all three as “the user’s token” would erase the very boundary the App needs to audit.
13.8 Hybrid is a Board choice, not an Event location
Section titled “13.8 Hybrid is a Board choice, not an Event location”Boards support Local, Remote, and Hybrid execution modes. App Events support only Local or Remote.
- A Local Board forces its App Events local.
- A Remote Board forces them remote.
- A Hybrid Board preserves the Local or Remote choice made for each App Event.
- The web client dispatches through the remote backend; Desktop and Studio can execute locally.
- A remote REST App Event is Remote by definition.
The product-level reason for Hybrid is simple: builders should be able to work locally in Studio while the web experience runs on governed remote infrastructure. That does not mean half of one invocation runs on a laptop and the other half in the cloud. One execution has one location. Nodes inside it may call remote systems, but the Flow run itself is not teleported between runtimes.
13.9 Pin the contract before other people depend on it
Section titled “13.9 Pin the contract before other people depend on it”An App Event can follow Latest or target a numbered immutable Flow version. Latest is useful while authoring because every saved Board change is immediately exercised. It is a poor default for a production API whose callers expect stability.
Use this release sequence:
- Develop and test against Latest.
- Save a numbered Flow version after the Page, Quick Action, and REST handler pass their cases.
- Point the production App Events at that version.
- Run REST setup and verify its route, authentication, OpenAPI document, and one failure case.
- Promote the change through the organization’s admin/builder review.
- Keep the prior version available for an explicit rollback.
A caller cannot override the App Event’s configured Flow version. REST registrations additionally have an App Event setup version and a pointer to the last successful setup. These identifiers solve different problems: the Flow version freezes authored logic; the setup version selects the published route configuration.
Do not describe weighted canary rollout as a current guarantee. The App Event model can store a canary target and weight, but the audited invocation paths select the primary target and do not perform weighted routing. Canary configuration is product intent until that selection is executed and tested end to end.
13.10 The complete App checklist
Section titled “13.10 The complete App checklist”The Incident Desk is complete enough to hand to another team when all of these questions have answers:
- Entry: Which Board event begins each use case?
- Contract: What typed values enter, and what structured value returns?
- Interface: Is the caller using a Page, Quick Action, schedule, REST route, or another surface?
- Identity: Who or what is calling, and what further domain authorization is required?
- Credentials: Which secrets may this execution use after entry?
- Location: Is this App Event Local or Remote?
- Release: Which immutable Flow version and successful setup are live?
- Evidence: Where does a successful run, failed run, or pre-run rejection appear?
The last item intentionally exposes a current gap instead of concealing it. A complete platform should preserve the evidence for a rejected request as deliberately as it preserves a failed node.
The larger pattern is already in place. The React prototype makes the intended experience tangible. A2UI keeps the production interface structured. The Quick Action and Cron adapters reuse the same decision without pretending their callers are alike. The REST setup makes routes, authentication, documentation, and release behavior visible. And every execution that reaches the Flow still returns to the same graph where an operator can identify the responsible block.
That is the step from a callable Flow to an App: not more hidden framework code, but explicit boundaries around logic that remains readable from both sides.