HTML View
Actionmacwindow
action.html_view — appears as HTML View in the Actions palette
Pauses a running flow and opens a page the flow itself wrote — an AI node's HTML, a template you pasted in, a script's output — in its own window. The page's buttons are the node's outlets: the flow continues carrying whatever the page sent back.
It is the blank-canvas step. User Input asks a question; the Mid-Flow Form asks several; Pick from List asks which of these. HTML View lets the page decide what to ask — a meeting reminder with Join / Snooze, a side-by-side compare with Approve / Reject, a tiny dashboard, a one-off tool an AI node generated a moment ago.
Only the branch that reached this node waits; the rest of the flow is untouched.
The page talks back with watchflows
Every page opened by this node has a small window.watchflows object waiting before its first script runs:
<button onclick="watchflows.emit({ action: 'join', meeting: { id: 'abc-defg' } })">Join now</button>
<button onclick="watchflows.emit({ action: 'snooze', minutes: 5 })">Snooze 5 min</button>
<button onclick="watchflows.dismiss()">Dismiss</button>
watchflows.emit(payload)— the page has answered. The flow leaves by Submitted with the payload undersubmittedas real structure: an object stays an object ({{submitted.meeting.id}}works), a list stays a list, a number stays a number. Call it with nothing andsubmittedis present but null.watchflows.dismiss()— the page is done without an answer. The flow leaves by Dismissed. The window's close button and esc do the same thing (unless you switch that off below).watchflows.update(state)— the page records where it got to, without answering. The window stays open and the flow keeps waiting. Only the most recent one is kept, and it rides out as{{state}}on either outlet — so a page that gets closed still reports what happened instead of losing it.watchflows.heartbeat()— “still going.” Only needed by a page that runs longer than the wait without anyone touching it — a countdown, a slideshow, a dashboard. Call it on a timer. Typing and clicking inside the page already count on their own, so a page somebody is using never needs this.
The page is self-contained. Inline styles, inline scripts and data: images all work; anything that needs the network — a CDN stylesheet, a web font, a fetch — is refused and simply never arrives. Write the page the way you would write an email: everything it needs is in it.
The HTML field opens in the HTML Editor. Click ↗ on the field for highlighted markup beside a live render of the page, with your test values filled in. In the inspector the field stays a fixed height and scrolls, so a long page doesn't push the window settings out of reach.
It is a real page, so it can behave like one. localStorage and sessionStorage work, and so does single-page routing with history.pushState — a page can hold state and move between views without ever leaving. What it cannot do is navigate: a link to another site, a redirect, a form submit are all refused. The view renders one page; it never browses.
What it remembers, and what it doesn't. Anything the page stores in the browser lasts only while the window is open — close it and that storage is gone. For something that should outlive the run, like a high score or a saved preference, get it out of the page: watchflows.emit() when it answers, or watchflows.update() as it goes. Then write it with a Database node; the next run reads it back and drops it into the page with {{…}}. That way the value lives in your flow's payload, where you can see it.
A game is the clearest example. Call watchflows.update({score}) each time the score changes and wire a Database node behind Dismissed. Now closing the window mid-game still records the score — the flow gets {{state.score}} whether the player finished or just walked away.
Three ways to take the screen
- Window — an ordinary window, centred on the screen your pointer is on. Watchflows comes to the front. For pages you read and act on.
- Floating card (HUD) — a small always-on-top card in the top-right corner that does not steal focus from the app you are in. Its buttons work without Watchflows becoming active. For glanceable things: a countdown, a status, a two-button question.
- Full-screen takeover — the page covers the whole screen, over everything, including another app's full-screen Space. There is no close button. esc leaves by Dismissed while Closing the window counts as Dismissed is on; switch it off and only the page's own buttons end it. The meeting-reminder move: use it when the page really must be answered.
Window and Floating card can also be chromeless: no title bar, no frame — the page's own edges are the window's shape, so a rounded card with a shadow is exactly what appears. Drag it by any part of the page. Give the page a transparent body — no background, no padding — and draw one rounded root element with its own background and padding: the window shrinks to that element and it floats on the desktop with a shadow, like a widget. A page that paints its body instead is shown edge to edge at the configured size, padding included, with the corners rounded for it.
Ports
Two ways out, and only one of them ever fires. Work you wire behind Submitted cannot run on an answer nobody gave.
| Direction | Name | Data Type | Description |
|---|---|---|---|
| Input | Input | Any | Incoming payload for HTML field interpolation — the default {{html}} reads the page an upstream HTML Template step rendered |
| Output | Submitted | Any | The page called watchflows.emit(). Carries the incoming payload plus submitted and submittedAt |
| Output | Dismissed | Any | The page was closed without an answer. Carries the incoming payload unchanged — neither key exists on this path |
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
| HTML | Text area | {{html}} | The page. {{variables}} are filled from the incoming payload, so the default {{html}} shows the page an upstream HTML Template step rendered with no config needed here — or paste a page directly. A page that renders to nothing fails the node before anything opens, telling you to put {{html}} in it after an HTML Template step, or paste a page into the node. |
| Title | Text | — | The window title. Filled from the payload too. Not shown when chromeless or full-screen. |
| Window | Dropdown | Window | Window, Floating card (HUD), or Full-screen takeover — see above. |
| Chrome | Dropdown | Title bar | Title bar is a normal macOS window. None is chromeless: the page's own edges are the window. Hidden for the full-screen takeover, which is always chromeless. |
| Width / Height | Number | 720 × 520 | The window's size in points. Hidden for the full-screen takeover. |
| Closing the window counts as Dismissed | Toggle | on | On: the close button and esc leave by Dismissed. Off: the window cannot be closed — no close button, esc and ⌘W do nothing — and only the page's own emit / dismiss, the wait running out, or stopping the flow end it. The takeover has no close button, so there this toggle governs esc alone. |
| Give up after | Number (minutes) | 0 | How long to go without hearing from the page before the node fails — not how long the page may stay open. 0, the default, never gives up. Anything above 24 hours is treated as 24 hours; use 0 for no limit. See below. |
Output Variables
Both keys exist only on the Submitted path. The variable picker offers them downstream of either outlet, so read them behind Submitted — on the Dismissed path they are genuinely absent, and {{submitted}} there resolves to nothing rather than to an earlier page's answer.
| Variable | Type | Description |
|---|---|---|
| submitted | Any | Exactly what the page passed to watchflows.emit(), as real structure. Navigate an object with {{submitted.action}}, {{submitted.meeting.id}}; a list with {{submitted.0}}. |
| submittedAt | String | When the page answered, ISO-8601. |
Both keys are merged over the incoming payload, so everything upstream — including the html that was shown — still flows through underneath.
Closing the page is not a failure
Dismissing takes the Dismissed wire, not the Failure wire. Closing a page is a normal outcome with its own path — the run finishes as completed, and a wired Failure outlet does not receive it. Leave Dismissed unwired and the flow simply ends there.
The fork is real. Whichever outlet is not taken is skipped in full. A Send Email wired behind Submitted can never fire on an answer nobody gave.
Stopping the flow closes the page — as a cancellation, not a dismissal. Stop, quitting the app, or switching the flow off takes the window down and cancels the run outright; neither outlet fires.
One page per node at a time. A second run of the same flow reaching the same HTML View while its page is still open is cancelled rather than stacking a second window behind the first.
By default the node waits as long as it takes. Most flows with a page in them are run on demand by the person who is about to look at it, and a page that stays open until they answer is the point — the same as a Form. Set Give up after for a flow that runs unattended — a 3 am Schedule fire, a webhook, an agent run with nobody watching. While a page is open the flow cannot run again, so a page nobody answers would quietly block every later trigger; a limit turns that into a red line in the run log instead. Running out of the wait fails the node; it does not take the Dismissed wire.
When a wait is set, it measures silence, not age. The clock restarts every time the page is heard from, so a page somebody is using never times out however long they take — typing, clicking and scrolling inside the page all count on their own, and so does every watchflows.update(). Only a page that runs for longer than the wait with nobody touching it needs to speak up, by calling watchflows.heartbeat() on a timer.
If the wait does run out, the node fails and no payload comes out — but the last thing the page reported through watchflows.update() is written into the run log, so an unattended page's work is still recoverable rather than silently gone.
A disabled HTML View continues via Dismissed. No page was shown, so the payload carries no submitted and the Submitted branch stays untaken.
Example
A meeting reminder: one minute before an event, an AI node drafts the event's details, HTML Template turns them into a page with Join / Snooze buttons, the takeover shows it, and Join opens the link.