The SDK
FlowFn injects a window.flowfn object into every playground iframe before your JS runs. It's the only supported bridge between your code and the FlowFn host page.
flowfn.data — sheets
flowfn.data.sheets()- Returns an array of sheet slugs available in this playground.
flowfn.data.get(slug)- Synchronous read of the rows currently cached in the iframe — usually the first page for large sheets. Returns an array of row objects keyed by column.key plus an internal
_id. Pair withrowCount+fetchbelow when you need every row. flowfn.data.fetch(slug)- Asynchronous. Pages any rows not yet cached into the cache and resolves with the full row array (same shape as
get). Concurrent calls for the same slug are de-duplicated. Use wheneverrowCount(slug)exceedsget(slug).length. flowfn.data.rowCount(slug)- Returns the server-side total row count as of the iframe's last load. May exceed the currently-cached
get(slug).lengthfor large sheets, and won't reflect rows added by other visitors or workflows until the iframe reloads. Returns0for unknown slugs. flowfn.data.columns(slug)- Returns the column definitions for the given sheet:
[{ id, key, name, type }]. flowfn.data.has(slug)- Boolean — does this sheet exist?
flowfn.data.insert(slug, cells)- Append a row. Gated by the sheet's Allow inserts opt-in. Returns the materialised row
{ _id, ...cells }. Rejects withsheet_write_disabledwhen off,sheet_fullat the plan's row cap,invalid_cellswhen cell types fail validation. See Data sheets. flowfn.data.update(slug, rowId, cells)- Patch an existing row's cells. Gated by Allow updates. No per-row ownership — any visitor can mutate any row when this is on. Cells you omit stay untouched.
flowfn.data.delete(slug, rowId)- Remove a row. Gated by Allow deletes. Returns
{ ok: true }. This is a soft delete — the row leaves all SDK reads immediately but is recoverable by the owner from the Data tab's Recently deleted panel for 90 days. There is no public restore.
flowfn.upload — visitor file uploads
flowfn.upload(fileOrInput, options?)- Upload a file from a public visitor. Accepts a
File, aBlob, or an<input type="file">element. Returns a Promise resolving to{ id, url, filename, content_type, size }whereurlis a freshly-signed read URL (~1h TTL). Rejects withuploads_disabled/unsupported_type/file_too_large/daily_upload_cap/rate_limited. Off by default — flip the editor header's Uploads toggle to enable. See User file uploads.
flowfn.snapshot & flowfn.fire — workflow triggers
flowfn.snapshot()- Returns the current values of all input, text, and js_value @keys as an object:
{ name_input: 'Alice', message_text: '...' }. flowfn.fire(triggerKey)- Fires a workflow trigger by @key. Auto-collects a snapshot. Click handlers on registered trigger @keys fire automatically — call this only when you need to fire from custom code.
flowfn.onResponse(callback)- Subscribe to trigger results. The callback receives
{ trigger_key, workflow_run_ids?, error? }. Returns an unsubscribe function.
flowfn.invoke & flowfn.onInvokeResult — bound actions
flowfn.invoke(keyName, inputs)- Runs a single platform-tool or AI action bound to a platform_tool_action / ai_tool_action @key. Returns a Promise that resolves with
{ status: 'completed', key, outputs }or rejects with a sanitised error code (rate_limited,invalid_inputs,handler_error,invocation_timeout, ...).inputskeys must match the binding's declared input names (which default to the action's field keys but can be renamed in the drawer to avoid collisions). - Auto-fire on click
- Clicks on any
[data-key]whose key is an action type auto-invoke. The SDK snapshots same-named @keys and sends only the declared inputs — extra fields are stripped. No need to callflowfn.invokemanually for click-driven actions. flowfn.onInvokeResult(callback)- Subscribe to results from any action invocation (manual or auto-fired). The callback receives
{ ok: true, key, status, outputs }on success or{ ok: false, key, error }on failure. Useres.keyto branch when several action keys share one playground. Returns an unsubscribe function.
flowfn.fetchData — registered reads
flowfn.fetchData(refKey, inputs?)- Runs a platform-tool action you registered under the Fetch tab by its
ref_key. Returns a Promise resolving to the action's outputs, or rejecting with an Error whose.codeis the server-side error code. Unlikeinvoke,ref_keyis a plain identifier (not a secret) — authentication is by allowlist membership + the playground's public access policy (published / password / embed gates). Use for reads (list/get); useinvokefor writes. See Fetch entries.
flowfn.server.call — server-side code
flowfn.server.call(name, args?)- Runs a function you wrote in the Server tab and declared in the Functions panel.
argsare validated against the function's declared inputs, then the function runs on FlowFn with access to your sheets, registered tools/fetch entries, and agent/workflow dispatch. Resolves with the function's return value, or rejects with an Error whose.codeis the server-side code (not_found,invalid_inputs,rate_limited,daily_cap_reached,TIMEOUT,RUNTIME_ERROR). Any function you declare and enable in the Functions panel is callable both from your editor preview and by visitors of your published page. See Server-side code.
flowfn.location — page URL hash and query
Your playground runs in a sandboxed iframe, which has its own internal address — so window.location.search inside it is always empty and won't show the query string of the page your visitor is on. FlowFn forwards the published page's URL hash and query string to your code instead:
flowfn.location.query- The parsed query parameters as an object. For a visitor on
your-site.flowfn.com/?ref=email&id=7, this is{ ref: 'email', id: '7' }. flowfn.location.search- The raw query string (e.g.
?ref=email&id=7). flowfn.location.hash- The URL fragment (e.g.
#support). For convenience this is also mirrored ontowindow.location.hashbefore your code runs, so existingwindow.location.hashcode and in-page#anchorlinks work as you'd expect.
Deep links work automatically. If the page is opened with a fragment that matches an element id — e.g. your-site.flowfn.com/#pricing with a <section id="pricing"> on the page — FlowFn scrolls that element into view on load. No code needed.
flowfn.currentPage — which page you're on
flowfn.currentPage is the current page's slug as a string — '' (empty string) on the home/index page, and always the canonical lowercase kebab-case slug (e.g. about-us) on every other page. Use it in shared site JS for page-conditional logic, like highlighting the active nav link:
// shared JS — mark the current page's nav link
document.querySelectorAll('nav a[href]').forEach((a) => {
const slug = a.getAttribute('href').replace('/', '');
if (slug === flowfn.currentPage) a.classList.add('active');
});
flowfn.state — shared client state
A lightweight key/value store for state that must survive navigation between the pages of a multi-page site (a cart, a checkout in progress, a wizard step). The sandbox can't use localStorage, so FlowFn keeps it for you, namespaced per playground and shared across all of that site's pages. Values must be JSON-serialisable; every method returns a Promise. For shared, queryable, or server-side data use Data sheets instead.
flowfn.state.set(key, value, options?)- Stores a value. By default it lives in the browser session (cleared when the tab closes); pass
{ persist: true }to keep it across visits. flowfn.state.get(key)- Resolves with the stored value, or
nullif unset. flowfn.state.remove(key),flowfn.state.keys(),flowfn.state.clear()- Delete one key, list all keys, or wipe this site's state.
await flowfn.state.set('cart', [{ id: 'sku_1', qty: 2 }]);
const cart = await flowfn.state.get('cart'); // [{ id: 'sku_1', qty: 2 }]
flowfn.rewire — for dynamic DOM
If your JS adds elements after the page loads, call flowfn.rewire() to re-apply data-key attributes to anything matching a registered selector. Idempotent — safe to call repeatedly.
Sandbox notes
The SDK runs synchronously before your JS, so top-level calls work:
// runs at script load — flowfn is already defined
const rows = flowfn.data.get('users');
document.body.innerHTML = JSON.stringify(rows);
The sandbox blocks browser storage APIs (localStorage, cookies, IndexedDB). For lightweight client state that survives page navigation use flowfn.state (above); for shared or persistent data use Data sheets or a workflow trigger. Because the iframe has its own internal address, read the page's URL parameters via flowfn.location rather than window.location.search.