# 13. Events, Interfaces, and Complete Apps

> Expose one typed Flow through an interactive Page, Quick Action, Cron schedule, and authenticated REST API with explicit identity, versions, and evidence.

- **Document type:** Book chapter
- **Canonical HTML:** [https://book.flow-like.com/part-2/13-events-interfaces-complete-apps/](https://book.flow-like.com/part-2/13-events-interfaces-complete-apps/)
- **Markdown alternate:** [https://book.flow-like.com/part-2/13-events-interfaces-complete-apps/index.md](https://book.flow-like.com/part-2/13-events-interfaces-complete-apps/index.md)
- **Book:** FlowBook — The FlowScript Book
- **Edition:** Open edition · 2026
- **Publisher:** Flow-Like
- **Language:** en
- **Topics:** Flow-Like Apps, App Events, A2UI pages, Quick Actions and Cron, REST APIs, authentication
- **Chapter:** 13
- **Part:** Part II — Thinking and Writing in Flows
- **LLM index:** [https://book.flow-like.com/llms.txt](https://book.flow-like.com/llms.txt)

---

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

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

Our shared decision has a deliberately small result:

```flow
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:

```flow
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:

```flow
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

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.

> **Interactive example:** The canonical HTML chapter includes a local Incident Desk demonstration. [Open the interactive version](https://book.flow-like.com/part-2/13-events-interfaces-complete-apps/).

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:

```json
{
  "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

The Quick Action can expose the three ordinary variables declared in the fixture:

```flow
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:

1. a permitted invocation/runtime override;
2. the App Event's variable override; and
3. 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:

```flow
@description("API key configured on the REST App Event")
@secret
const incidentApiKey: string
```

Its 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

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:

```flow
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:

1. Target `configureIncidentRest`.
2. Select **REST**, **Remote**, and the intended **Public** or **Internal** exposure.
3. Pin a tested numbered Flow version.
4. Configure the `incidentApiKey` secret override.
5. Choose a stable alias such as `incident-desk`.
6. 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:

```text
POST /r/incident-desk/triage
GET  /r/incident-desk/openapi.json
GET  /r/incident-desk/docs
```

## 13.6 The HTTP boundary is structured—and still evolving

Call the route with an API key and JSON body:

```http
POST /r/incident-desk/triage
x-api-key: <configured secret>
content-type: application/json

{
  "systemId": "payments",
  "report": "Production is on hold",
  "impact": "production-stopped"
}
```

The handler returns one object:

```json
{
  "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

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

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

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:

1. Develop and test against Latest.
2. Save a numbered Flow version after the Page, Quick Action, and REST handler pass their cases.
3. Point the production App Events at that version.
4. Run REST setup and verify its route, authentication, OpenAPI document, and one failure case.
5. Promote the change through the organization's admin/builder review.
6. 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

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.

---

## Reading navigation

- [Previous: Functions, Layers, Handlers, and Caching](https://book.flow-like.com/part-2/12-functions-layers-handlers-caching/index.md): Turn repeated node graphs into reusable typed functions, distinguish layers from handlers and Events, and design safe cache keys and invalidation.
- [Next: Board ⇄ AST ⇄ Text](https://book.flow-like.com/part-3/14-board-ast-text/index.md): Learn how Flow-Like lowers a Board to a typed AST, renders canonical FlowScript, reconciles semantic edits, applies atomic patches, and preserves identity.
- [Complete contents](https://book.flow-like.com/contents/index.md): Return to the full FlowBook reading plan.
