Lesson 12 — 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, TTL.
Two modes — one-shot vs supervised
Lesson 32 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 04), 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 05)
:fn-typed slot → fn-picker
Live validation runs as you type — ✓ OK or ✗ <reason> next
to the input. Submit is disabled until every required field is
valid.
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 a
:stringfield and the hintExpected: text. Typehello world. - Click
Run. Result11appears.
If you create a tutorial fn-def with multiple free args, the
form lists them all. 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 fn → 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). Categories:
| Effect | Means |
|---|---|
:db |
Writes / reads graphden's storage |
:network |
Outbound HTTP / TCP |
:io |
Disk / filesystem |
:env |
Reads OS env vars |
:time |
Reads wall-clock time |
:random |
Non-deterministic input |
:process |
Spawns supervised background work (service-eligibility marker) |
:state |
Mutates in-graph state (:swap / :reset on a :cell / :atom) |
:raw-sql |
Raw SQL escape hatches (:pg-query & co) that bypass the storage protocol |
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 lens — Lesson 03) 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 kept in memory only — visible for
the next few minutes (TTL), then garbage-collected. Tick the
Save to history checkbox in the pane and the result
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; oversize results write a placeholder with a download link):effects(the actual effect set the runtime saw, NOT the declared one — drift between the two surfaces in the editor):error+:error-dataon failure (capped at 4 KB)
Persisted executions show up in the runs list under the form (see below) and survive restarts.
The history list
The Runs tab doubles as this fn's history: below the form sits the list of its PERSISTED runs (in-memory non-persisted runs never appear 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 (across branches). Save to history is what
controls whether a pure run's result survives the in-memory TTL
— 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 in place ofRun. - From the history panel later — the running row has a
Cancelaction.
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.
TTL — what gets garbage-collected
Non-persisted executions live in an in-memory atom with a configurable TTL (default 30 minutes). After the TTL elapses, the row is dropped. Persisted executions are immune — they live in PG until you delete them.
Two side effects of this:
- Only persisted runs appear in the History panel — a pure
run without
Save to historyleaves no visible trace once its in-memory row expires. - The TTL also bounds the cancel window — once a row's garbage-collected, you can't cancel an execution you no longer have a handle for.
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 + a fresh history row. - Run again with
:string = "world"(also no persist). - Tick
Save to history. Run with:string = "graphden". - Refresh the page. The first two runs are gone; the third (persisted) is still in history.
- 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 (off by
default — tracing adds a small capture cost to the run). When
checked, the run records which fns it traversed: one entry per
internal fn call, with the time each call took and whether it
was served from the per-run cache.
Try it:
- Open ▶ on a composed fn (anything that references other
fns —
:str-lenwrapped in your own fn-def works). - Tick
Trace path(andSave to historyif you want the trace to survive the page). - Run. Under the result you get a
Show path on canvasbutton — click it. - The traversed fn cards light up with a blue ring, each
wearing a small badge:
12ms(time spent in that fn),3× 12ms(called 3 times, 12 ms total — loops and shared subtrees re-enter the same fn), orcache(the result was reused, no time spent). - A small panel at the bottom of the screen summarises the
path. If the run traversed fns that aren't currently drawn
on the canvas, it says
not on canvas: N fns— hover it for their names. Click✕ clear(or navigate anywhere) to restore normal rendering.
Persisted traced runs keep their path: in the History panel,
rows with a recorded path show a path button that replays
the same highlight.
Capturing values
By default the trace records only fn ids, timings and cache flags — never the data flowing through. When you need the data too, there is a second step:
- Tick
Trace pathfirst. That unlocks the+ capture valuescheckbox next to it (it stays greyed out otherwise). - Tick
+ capture values. A confirmation dialog appears with an estimated cost line — something likeEstimated cost: up to ~48 KB (~12 fns in this fn's reach). This is the expensive mode, so graphden asks explicitly; declining the dialog unticks the box. - Run, then
Show path on canvas. Traversed cards now wear a second chip under the timing badge:= value. Click it to see that fn's captured return value, pretty-printed. When a fn ran several times, you see the last value (the popover saysLast of N captured invocations).
Limits you may run into, each reported rather than silent:
- A single value larger than 4 KB is not captured — the chip
shows
= 4KB+and its popover explains the cap. - If all captured values together exceed the total budget
(16 MB), the oldest entries are dropped first and the
bottom panel says
some values dropped. - A fn that touches
:secret-typed data (lesson 13) shows a redsecretbadge, no timings and no value chip — its value is never even read by the capture machinery, in either mode.
What the trace never contains:
- Values, unless you explicitly confirmed capture. A
plain
Trace pathrun records fn ids, timings and cache flags only. - Secrets. The capture pipeline redacts secret-touching fns at record time, values included.
What we glossed over
- Branch-aware execution — the active branch picks which version of the fn-graph runs. Lesson 20 (already written) covers branches.
- Service-mode execution — fns marked as services run forever, supervised by graphden. Lesson 32.
- HOF call shape — how internal refs get their free args bound at compile time vs call time. Lesson 06.
Next
Lesson 32 — Services (already written)