Lesson 15 — Executing a fn: free-arg form, history, cancel
Goal: by the end of this lesson you can click ▶ on any
fn-card, supply its free arguments, inspect the result, replay
from history, and cancel a running execution.
Concepts introduced: execute, free-arg form, effect-gate,
persist toggle, execute history, repeat, cancel, retention.
Two modes — one-shot vs supervised
Lesson 35 introduces SERVICES — fns graphden keeps running
forever. This lesson is about the OTHER mode: one-shot
execution. You click ▶, the fn runs, you see the result, the
runtime moves on. No supervision, no restart.
Run paths:
| Path | When to use |
|---|---|
▶ Run in the row's ⋯ actions popover |
Interactive — you click + view the result |
| The Inspector's Runs tab (right panel) | The selected fn's own run history, live — there's no separate Run page; running is always the ▶ action, history lives here |
POST /api/execute |
Programmatic — scripts / curl / other services |
| Internal calls (refs in fn-graph) | Happens automatically when one fn refs another at runtime |
▶ and /api/execute are the same code path; the button just
builds the request body for you.
The free-arg form
If the fn has any FREE arguments (slots that no ancestor binds — see lesson 05), the Run pane shows a form to supply them. Field types match the slot's declared type:
:textslot → text input:intslot → number input:boolslot → checkbox:portslot → number input + the refinement's range hint- Record / list slots → nested form (see lesson 08)
:fn-typed slot → not entered here: the pane says this argument is a function, resolved from the graph at run time
Live validation runs as you type — a ✓ or ✗ <reason> marker
next to the input. The marker is advice, not a lock: the server
re-checks the args on submit and rejects a mismatch outright.
Try it
- Open
:str-lenin the editor. It has one free arg:string(declared :text). Click▶. - The Run pane opens in the right panel's Runs tab, with one
field named after the free slot,
:string. Typehello. - Click
Run. Result5appears.
If you create a tutorial fn-def with multiple free args, the
form lists them all — the optional ones folded under an N optional
row you unfold when you want them. The placeholder + edges on the
card mirror the form so you can see what's needed at a glance.
Typed result representations
The result pane doesn't always print text — it first asks what type the result is and picks a representation:
- A numeric series (
[:list :numeric]— ints or floats) renders as an inline sparkline with a value-count caption instead of a bullet list. - A list of records (keyword-keyed maps — a storage query, an API selection) renders as a table: columns from the first record's keys, a visible row-count caption.
- A fn whose return type is
:hiccup-node(a component — a form, a card, anything you'd normally insert into a page) renders as a Component preview: the markup, live, inside a fully sandboxed frame (no scripts, no access to your session). You see what the component looks like without inserting it anywhere. - Everything else falls back to the shape panes you'll see below (list bullets / record table / scalar).
The dispatch runs on the fn's declared or inferred return type —
the same type you see in the inspector's RETURNS strip — with the
runtime value's shape as a fallback, so even an untyped sketch
returning [3 1 4] sparklines.
Try it
- Open
:range(core.collections) and click▶. Setendto12, leavestart/stepat their defaults. - Run. Instead of twelve bullet rows you get a rising sparkline
and the caption
12 values. - Create a component fn-def: New graph… → name
hello-card, parent:wrap-element, bind:tagtodivand:contenttohello from a card. - Click
▶→ Run onhello-card. The pane shows Component preview — your<div>rendered as markup in a sandboxed frame, because the fn's inferred return type is:hiccup-node. - Self-host only: open
/previewwith no parameters — the components gallery: every:hiccup-node-returning fn as a card, pure zero-arg components rendered live, the rest one click away. - Self-host only: the preview caption also carries "Open
interactive preview ↗" — it opens
/preview?fn-id=…in a new tab, where the component runs as a REAL page: htmx swaps fire, forms submit, custom scripts run. An effectful component first shows a confirm page mirroring the Run gate. (On multi-tenant cloud this link is hidden and the route answers 403 — live org-authored scripts must not run on the editor origin; that's what the apps domain is for.)
Representations are themselves graph code: the type→repr table is
the :_value-repr-registry fn-def (app.reprs namespace), each
repr a pure value → hiccup fn-def whose output is sanitized
before the editor inlines it. On a self-hosted deployment an admin
extends the system by adding a repr fn-def plus one registry row —
no server change. (On cloud the shipped registry is read-only for
tenants.)
The effect gate
Every fn carries a set of effects it transitively touches
(computed from the impl + propagation through refs) — one keyword
per category, :db, :network, :env and the rest; lesson 16
lists all ten and explains where they come from.
When you open the Run pane for a fn with EFFECTS, it shows
a warning banner — side effects: followed by one chip per
category — plus a confirm checkbox:
side effects: [network] [db]
[ ] I understand this will produce side effects
[Run] ← disabled until the box is ticked
The gate prevents accidental side effects: Run stays disabled until you tick the acknowledgement. For pure fns (no effects in the registry) neither the banner nor the checkbox appears — the pane opens straight to the form (or the "No free arguments" note) with Run enabled.
The type-error gate
The effect gate's sibling: a fn whose current branch carries recorded TYPE diagnostics (the ⚠ badge / the Explorer's ⚠ type errors filter — Lesson 04) is refused at submit. The run comes back rejected with a clear message — "Execution refused: fn '…' has unresolved type errors — …" — naming the fn and the first error. There's nothing to acknowledge away here: fix the fn or its bindings (the fixing save clears the diagnostic), then run. This is the flip side of type errors not blocking saves: you can keep a half-typed sketch in the graph, but it won't execute.
The persist toggle
By default, PURE runs are not stored: the result shows in the
pane and that is all — no row is written, so there is nothing to
come back to after a reload. Tick the Save to history checkbox
in the pane and the run writes a :fn-execution row.
Effectful runs don't get a choice: the checkbox comes pre-ticked and locked ("Automatically saved — runs that produce side effects are always persisted for audit trail"). The persisted row carries:
:fn-id+:fn-version-id(frozen at start time so the audit trail survives later fn-def edits):args(the resolved free-arg values, capped at 256 KB):result(capped at 5 MB; an oversize result is stored asnilwith:result-truncated? true):effects(the actual effect set the runtime saw, NOT the declared one — drift between the two surfaces in the editor):error(capped at 4 KB) +:error-data(64 KB) on failure
Persisted executions show up in the runs list under the form (see below), survive restarts, and are swept by retention (below).
The history list
The Runs tab doubles as this fn's history: below the form sits
the list of its PERSISTED runs (a run without Save to history
leaves no row, so it never appears there). Each row shows:
- The args used
- The status (
succeeded/failed/cancelled/pending— a still-running persisted row ispendinguntil it resolves) - The result (truncated to a one-liner)
- A
Repeatbutton — re-fills the form with the same args so you can re-run
History is per fn, for the version the current branch resolves —
the one ▶ would run now. Save to history is what decides
whether a pure run gets a row at all — effectful runs are always
there.
Cancel
Long-running executions (an :http-get that hangs, a
:sleep for 30 minutes) can be cancelled:
- From the Run pane during the run — a
Cancelbutton appears while the run is pending. - Programmatically —
POST /api/execute/<id>/cancel(below).
Cancel sets a flag the executor checks at each ref boundary
(*cancel-check*). Already-running impls don't get
interrupted in flight, but no new sub-ref starts after cancel
fires. For most fns this means a clean rollback; for impls
that spawned external work (HTTP, threads) the cleanup is
impl-specific.
Programmatic execute
curl -X POST http://localhost:9002/api/execute \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-d '{"fn-name": "str-len", "args": {"string": "hello"}}'
Returns immediately with {:execution-id "..." :status "pending"}
when the fn is long-running; for fast fns the response carries
the result directly.
Poll status:
curl http://localhost:9002/api/execute/<id> ...
Cancel:
curl -X POST http://localhost:9002/api/execute/<id>/cancel ...
The shape of :result mirrors the in-memory return value —
JSON-encoded.
Retention — what gets swept
Persisted rows are not kept forever. An hourly sweep deletes
succeeded and cancelled runs after 7 days and failed
runs after 30 days; a row stuck in pending for over an hour
is flipped to cancelled first and then follows the 7-day rule.
Two consequences:
- Only persisted runs appear in the Runs tab — a pure run
without
Save to historyleaves no row, so there is nothing toRepeatlater. - Cancel needs a pending row. A run that outlives the inline
wait is written as
pending(whether or not you ticked the box) and hands back its id — that id is what theCancelbutton and/cancelact on.
Try it (the persist + history loop)
Prefer to be shown? This lesson exists as a guided in-editor tour: open the demo with the tour running (no sign-up), or pick “Interactive tutorial” in the editor's account menu.
- On
:str-len, run with:string = "hello"(no persist). See the result; the runs list under the form does not change. - Run again with
:string = "world"(also no persist). - Tick
Save to history. Run with:string = "graphden". - Refresh the page. The first two runs left no trace; the third (persisted) is in the list.
- Click
Repeaton the persisted row — the form pre-fills, you run again, get the same result.
Tracing an execution
The Run pane also has a Trace path checkbox (and, behind it,
+ capture values): the run records which fns it traversed, and
optionally what each returned. That is lesson 18's subject — the
canvas highlight, the step-through call tree, and the capture caps.
What we glossed over
- Branch-aware execution — the active branch picks which version of the fn-graph runs. Lesson 23 covers branches.
- Service-mode execution — fns marked as services run forever, supervised by graphden. Lesson 35.
- HOF call shape — how internal refs get their free args bound at compile time vs call time. Lesson 09.