Skip to content
Build

Interfaces & apps

An interface is a web page (HTML, CSS, and JavaScript) rendered inside your workflow. The workflow is the backend and the interface is the frontend: you feed data into the page with variable mapping, the page sends user input back with action mapping, and by chaining interfaces you turn a workflow into an app your team, or your customers, can use.

What an interface is

Inside a workflow, an interface is a regular node: it needs at least one incoming edge and runs once its predecessors complete or are skipped. It has no ports of its own, it is a linear step in the graph. The page itself renders in a sandboxed iframe, separate from the rest of the app, and it does not inherit the app's styles or theme.

An interface does not have to live inside a workflow at all. You can create one standalone (a landing page, a calculator, a small game) and it works the same way, minus the variable and action mapping to a run. Your interfaces are listed on the Interfacespage, which is not shown in the sidebar by default: open it from the sidebar's overflow menu, or tick it in the customize panel behind that menu to pin it.

Two directions of data

An interface connects to the workflow through two small maps you configure on the node: variable mapping feeds workflow data into the page, and action mapping sends user input back out.

Variable mapping: workflow to page

Map a friendly, generic name to a workflow expression. In the HTML template you use {{name|default}}; in JavaScript you read the same data from a single resolved object.

variableMapping
"variableMapping": {
  "userName": "{{mcp:fetch_user.output.name}}",
  "results":  "{{mcp:search.output.items}}"
}
html
<h1>Hello {{userName|there}}</h1>
<div id="results"></div>
<!-- in js_template: -->
<script>const data = window.__RESOLVED_DATA__; /* data.results, data.userName */</script>

Values are resolved when the page renders: a name that has no match falls back through a short chain, first the exact key, then the same key without its type: prefix, then a dotted drill into nested objects and arrays ({{photo.name}}, {{images[0]}}), then the pipe default, and finally a plain [label]placeholder as a last resort. A step that hasn't run yet (or failed) resolves the same way, through its default.

If a form on this same interface was already submitted once (in an earlier epoch of the same run), its fields pre-fill: matching name attributes on inputs, textareas and selects are populated with the previous submission automatically.

js_template and window.__RESOLVED_DATA__

Any <script> tag you write directly in the HTML template is stripped for security, and so is every inline event handler (onclick, onerror, and so on). All custom JavaScript goes in the node's js_template field, which is injected as its own script block and runs after the page renders:

js_template
(function () {
  var data = window.__RESOLVED_DATA__;
  var list = document.getElementById('results');
  try {
    (data.results || []).forEach(function (item) {
      var el = document.createElement('div');
      el.textContent = item.name;
      list.appendChild(el);
    });
  } catch (e) {
    // always guard JSON.parse / __RESOLVED_DATA__ access
  }
})();

window.__RESOLVED_DATA__holds every resolved variable, keyed by its friendly mapping name. Use it for loops, conditionals, and any DOM manipulation your template can't express with plain substitution. Your js_template script is injected last, after the platform's own scripts, so a thrown error in your JS never breaks them: the static HTML and the resolved variables are already rendered by that point.

Feeding a page from a Code step

A Code step's output is exposed downstream under an extra result key: whatever your code returned is read as {{core:<label>.output.result.<field>}}. If you map the whole output into an interface, the page receives that wrapper too.

Action mapping: page to workflow

Map a CSS selector to a single target key describing what should happen when the user interacts with that element.

actionMapping
"actionMapping": {
  "#search-form": "trigger:search:submit",
  "#chat-input":  "trigger:chat:message",
  "#next-btn":    "__continue",
  "#to-details":  "interface:details:navigate",
  "#next-page":   "__pagination:next"
}
Action mapping target keys
Target keyWhat it does
trigger:label:submitBinds the form submit event, collects every field with a name attribute, fires the trigger with that data.
trigger:label:messageOn a form, same as submit. On an input or textarea, Enter sends {message: value} and clears the field (Shift+Enter still inserts a newline).
trigger:label:clickBinds a click event and sends the closest form’s data (or none, for a standalone button).
interface:label:navigateSwitches the displayed page without touching the run. Frontend-only, no backend call.
__continueResolves the interface’s signal and advances the workflow to the next node.
__pagination:next|prevMoves to the next or previous page of the application. Frontend-only.

Only user-initiated triggers are legal action targets: manual, form, and chat. For __continue, the binding is a form's submit event when the mapped element is itself a <form>, otherwise it's a click; either way the element's closest form data (including files) is collected and sent along, and a standalone button with no surrounding form sends an empty payload.

Blocking vs just displaying

Whether an interface pauses the workflow is decided by exactly one thing: is __continue one of the action targets?

  • With __continue: the interface blocks. The node yields an INTERFACE_SIGNAL and the run waits (AWAITING_SIGNAL) until the user clicks continue. This is how you build a wizard.
  • Without it: the interface just displays. Successors run immediately, and the run may reach COMPLETED while the page stays interactive, which suits a results page the user can keep re-submitting.

INTERFACE_SIGNAL is the only conditionally blocking signal in the engine, unlike a wait timer, a user approval, or a webhook wait, which always block. If a non-blocking interface still has un-run successors and its __continue fires after the run already completed, the run reopens (COMPLETED to RUNNING), executes the remaining successors, and re-finalizes: it stays RUNNING if a new blocking signal appeared, or returns to COMPLETED otherwise.

navigate vs __continue

navigate compared with __continue
Aspect__continuenavigate
Advances the workflowYes, backend call, resolves the signalNo, frontend-only
Target scopeOnly triggers of this interface’s own DAGAny interface in the workflow, doesn’t need to share a DAG
Use it forWizards, one step at a timeTabs and multi-page apps that share state

The Application view

When a run reaches an interface, the workflow's side panel gets an Application tab next to the workflow view: the workflow canvas is hidden and only the interface pages show, one after the other. Which page you see is decided like this:

  • Page order follows the canvas from left to right: move an interface node on the canvas to move its page.
  • First page: the interface with Entry Interface turned on (isEntryInterface: true), otherwise the leftmost interface on the canvas.
  • While a run moves: the view follows the interface the run most recently reached (running or waiting for the user). Once the run is finished, it stays on the last interface that completed; it does not jump back to the entry page.

Building multi-page apps

  1. Wizard
    Chain interfaces in the DAG, each with a __continue action. Blocking keeps the run RUNNING throughout and advances one page at a time.
  2. Tabs and multi-page
    Use navigate so pages share the same run state without advancing the workflow.
  3. Carousel over a Split
    Put an interface right after a Splitnode and it runs once per item: each item gets its own signal, and the page renders as a paginated carousel. If the interface blocks, every item's signal must resolve before the run advances past it.
  4. Fork and merge
    Several interfaces in parallel behave as an implicit fork; a downstream merge waits for all of them (completed or skipped). Blocking interfaces keep the run going until every one's __continue fires; non-blocking ones auto-advance independently.

Runs and pages are addressed as (epoch, spawn, item index) triples: each trigger fire starts a new epoch and previous results stay browsable, and a Split ahead of an interface adds the item index as extra pages.

Build a two-page app

This example shows the same data on two pages, a summary and a details page, and lets the user switch between them without re-running anything.

  1. Produce the data
    In a new workflow, add a Manual trigger followed by a Code step labelled Load that returns the data your pages show.
  2. Add the first page
    Connect an interface labelled Home after Load. Map its data past the Code wrapper ("data": "{{core:load.output.result}}"), give it a button with id="to-details", map #to-details to interface:details:navigate, and turn on Entry Interface.
  3. Add the second page
    Connect a second interface labelled Details after Load as well, and place it to the right of Home on the canvas. Map the same data, and map a #to-home button to interface:home:navigate.
  4. Run it
    Run the workflow and open the Application tab. Home opens first; the buttons switch pages instantly because navigate never calls the backend. Neither page uses __continue, so the run completes and both pages stay interactive.

Rendering and authoring constraints

On top of the script stripping described above, every interface automatically gets a small set of injected system scripts: a height reporter so the parent can auto-size the iframe, a navigation gate, a broken-image fixer (broken <img> tags fall back to a transparent pixel to preserve layout), an optional auto-fit scaler, the action bridge, and the __RESOLVED_DATA__ injector. Links behave like this:

How links inside an interface behave
Link typeBehavior
In-page anchor (#section)Scrolls, allowed.
Scheme-less relative linkDoes nothing: a single embedded page has nowhere to navigate to.
External (http/https/mailto/tel, or //host)Gated: the viewer is asked to confirm, then it opens in a new tab.
javascript: or empty/# hrefBlocked.
window.open()Gated, allowed only under a genuine user gesture (a real click).

Authoring tips that avoid the common layout gotchas:

  • Include a fixed-width viewport tag, for example <meta name="viewport" content="width=1280">, whose width matches the interface's format (see below; 1280 when no format is set). The host does not pass a real device width to the iframe, so width=device-width misrenders.
  • Set an explicit background and text color on body: the page does not inherit the app's theme.
  • A fragment template (one that does not start with <!DOCTYPE html> or <html>) gets a small base stylesheet and centering CSS on bodyin the app's previews, so wrap a full-width or top-aligned layout in a single wrapper <div>. A complete HTML document gets nothing injected and keeps its own body layout.
  • Design desktop-first, with responsive breakpoints around 1024px, 768px, and 480px.
  • Google Fonts and Material Icons load fine through a normal <link> tag.

Interface format

An interface can declare a Format: the shape it is designed for. The format sets the dimensions of every screenshot, video, and preview of that interface. Pick a preset or enter a custom WIDTHxHEIGHT (each side between 16 and 2160 pixels).

Interface format presets
PresetSize (px)
classic1280 x 800
widescreen1920 x 1080
vertical1080 x 1920
square1080 x 1080
portrait1080 x 1350
mobile390 x 844
tablet820 x 1180
desktop1440 x 900
banner1500 x 500
social_card1200 x 630
a4_portrait794 x 1123
a4_landscape1123 x 794

Leaving the format unset is not the same as classic: with no format, a screenshot captures the whole page at 1280 pixels wide however tall it is, while classic captures exactly 1280 x 800 and crops anything below. Leave it unset for a long dashboard or report you want captured whole.

Files & images in the page

When an interface renders, every file reference reachable through variable mapping is automatically converted into a usable URL wherever it lands: in <img src>, <a href>, <video src>, or in window.__RESOLVED_DATA__. Map the file under any friendly name, then reference that name in the HTML or iterate a list of them in js_template. Its .name, .mimeType, and .size are safe to drill; its raw storage .path is not. See Files & storage for the file object itself.

Files are scoped to your workspace: any member can view a file the page points to, and a different workspace is denied. Your access token is never placed in the page's HTML.

Uploading files from a form

An interface form can include <input type="file" name="photo"> like any other field. On submit, the file is uploaded and stored the same way any other file is. The next step reads it as a normal file reference under {{trigger:<label>.output.photo}} (a few flat sidecar fields, file URL, name, size, and content type, are also emitted alongside it for convenience).

Interface node outputs

Every interface node always outputs its interface_id, its action_mapping, whether it is_entry_interface, and its resolved resolved_params. Four more outputs are opt-in, each turned on with a switch on the node. They are best-effort: a capture failure leaves the output absent without failing the run.

Optional interface node outputs
SwitchOutputWhat you get
Generate screenshotscreenshotA PNG capture of the rendered page as a file, sized by the interface format, for a downstream step to attach, email, or store.
Generate PDFpdfA PDF export of the rendered page as a file. PDF page size is A4 (default) or Letter, with a Landscape option. The interface format does not apply to the PDF.
Generate videovideoAn MP4 recording of the page’s animation. Recording stops when the page sets window.__DONE__ = true, or at the maximum duration (default 30 s, 5 to 120 s in the editor). Options: Video format (Auto uses the interface format, or Vertical, Horizontal, Square), Render mode (Smooth, frame by frame, the default; or Real-time), and Frame rate (24, 30 by default, or 60).
Expose rendered sourcerendered_html / rendered_css / rendered_jsThe page source as plain strings, each capped at 256K characters. Variables are filled in the HTML only; the CSS and JS come back exactly as written.

Reading what the user submitted

Downstream steps read a submitted action with {{interface:<label>.output.<action_name>.<field>}}:

text
{{interface:my_form.output.submit.name}}
{{interface:my_form.output.submit.email}}

Each submission is stamped with a fired_at timestamp automatically, and multiple submissions of the same action accumulate rather than overwrite one another. Firing a regular action (not __continue) returns immediately and the interface stays active and awaiting further input; only __continue resolves the blocking signal.

Editing an interface with the assistant

The assistant can create, read, list, update (replace a whole template), patch, and delete interfaces. A patch edits one template (HTML, CSS, or JS) with an ordered list of search and replace edits, applied all or nothing. After 10 consecutive successful patches on the same interface the assistant is told to stop and ask you what you want changed; an edit that fails to match writes nothing and does not count.

Applications

A workflow with interfaces becomes an application when you publish it to the marketplace. The Applications page lists the apps you published and the ones you installed from the marketplace. Like Interfaces, it is not shown in the sidebar by default: open it from the sidebar's overflow menu.

  • Filter with All, Installed, or Published, and by visibility (Public, Private).
  • Sort by Last executed, Recently added, or Name, mark favorites, and organize apps in folders.
  • Open an app to use it full page, with its pages in the Application view.
  • Select one published app to Update it (re-run the publish wizard) or create a Share link.

Share an application with a link

  1. Select the app
    On the Applications page, select exactly one app you published.
  2. Create the link
    Click Share link in the selection bar and copy the link.
  3. Manage it later
    Your links are listed under Settings > Public Access (Applications tab) and in the Shared tab of the notification bell, where you can copy or revoke them.

Anyone with the link can open and use the app without an account. The app runs under your account, so treat the link like a key and revoke it when you no longer need it.

Publishing a standalone interface

A standalone interface can be published to the marketplace too, with a title and a visibility of private (default, not listed), unlisted (reachable by link, not listed), or public (goes through platform review before appearing). An interface is its own landing page, there is no separate listing page to create. You can charge credits per use, or leave it free.

Troubleshooting

Interface troubleshooting
SymptomCauseFix
The page shows its empty state on every run, with no errorThe whole output of a Code step is mapped, so the data sits one level deeper, under result.Map past the wrapper: {{core:<label>.output.result}}. See Feeding a page from a Code step.
Clicking a mapped button or link does nothingThe selector in action mapping matches no element in the rendered page (nor any data-action attribute), so the binding is skipped without an error.Make the selector match your markup exactly, for example #next-btn for <button id="next-btn">.
A submit action never firesThe selector points at the submit button. A submit binding listens on the element it matches, and only a <form> receives the submit event.Map the form's own id, and keep the submit button inside that form.
A field is missing from the submitted dataOnly fields with a name attribute are collected. An id alone is not enough.Add name to every input, select, and textarea you want downstream.
Code in a <script> tag or an onclick attribute never runsScript tags and inline event handlers are removed from the HTML template for security.Move the code to js_template and attach listeners there with addEventListener.
Later steps run before the user has done anything on the pageThe interface has no __continue action, so it only displays and does not pause the run.Map a button or form to __continue. See Blocking vs just displaying.

Related pages