Embedding overview

Embed EmbedWorkflow UI in your app, declaratively on any stack or with the React or Vue package.

Overview

Embed EmbedWorkflow UI in your app: the full app, a workflow settings form, the connections UI, or a single field. You can do it declaratively on any stack, or with a framework package (React or Vue).

Runs in the browser

The UI always renders in the browser via the EWF runtime loaded from the CDN. Your backend's only job is to sign a JWT. There is no server-side rendering of EWF components.

Setup (once per page)

Include the loader

1
2
3
4
5
6
7
8
9
10
11
<script>
  !(function () {
    var e = (window.EWF = window.EWF || {});
    if (!e.invoked) {
      e.invoked = !0;
      e.queue = [];
      e.load = function () { e.queue.push(arguments); };
    }
  })();
</script>
<script src="https://cdn.ewf.to/ewf-loader.js" async></script>

Establish auth with a signed JWT

EWF.load(publicKey, { jwt }) establishes auth. Your public (publishable) key is browser-safe and separate from the secret key you use to sign the JWT server-side; never ship the secret key to the browser. Find both in your account settings.

1
2
const { pkToken, jwt } = await fetch("/api/ewf/embed-token").then((r) => r.json());
window.EWF.load(pkToken, { jwt });
Framework packages

Prefer the exported load() (import { load } from "@embedworkflow/react" or /vue) over window.EWF.load. It retries through the loader's brief startup gap so a fast call isn't dropped.

New users

Include discover: true in the JWT payload for a sub that doesn't exist yet. It upserts the user on first use; otherwise the API returns 401.

Two ways to embed

Declarative (class + data attributes)Framework packages (React / Vue)
Works withany stackReact or Vue apps
How it mountsEWF.load() scans the DOM for EWF__* classescomponents mount via window.EWF.mount
Best forstatic markup, quick embedsapps wanting typed props / events

Both render the same UI.

Declarative embeds

1
2
3
4
<div class="EWF__app" data-base-path="workflows"></div>
<div class="EWF__settings-form" data-workflow-id="wf_123"></div>
<div class="EWF__connections"></div>
<div class="EWF__field" data-workflow-id="wf_123" data-field-id="slack"></div>
AttributeApplies toMeaning
data-workflow-id / data-workflow-keysettings-form, fieldWhich workflow
data-field-idfieldField name or id
data-valuefieldInitial value (uncontrolled)
data-base-pathappBase route (no leading slash)
One-time scan

The declarative scan runs once, at EWF.load(). Elements added later (SPA route changes) aren't picked up. Use a framework package or the imperative API for those.

Embedding the full app (EWF__app)

  • data-base-path takes no leading slash ("workflows", not "/workflows").
  • Give the element an explicit, non-% height (a % height collapses to 0).
  • The app does client-side routing under the base path, so add a catch-all route for /{basePath}/* or navigation and refresh will 404.

Embedding a single field

Place one field from a workflow's form inside your own form (for example, a Slack channel picker). The field renders standalone, doesn't persist, and emits ewf:change on every change; your app collects the value and submits it. field matches the field's name or id; prefer the name.

The ewf:change event

Every field dispatches a bubbling CustomEvent named ewf:change on its host element:

1
2
3
4
5
interface EwfChangeDetail {
  fieldId: string;   // the identifier you embedded with, not EWF's internal id
  value: unknown;    // the stored scalar (channel id, boolean, string[]…)
  option?: { value: unknown; label: string };  // present for select fields
}

Advanced: imperative API (@embedworkflow/embed-core)

The framework packages are thin wrappers over a small imperative surface on window.EWF, exposed by the framework-agnostic core @embedworkflow/embed-core. Most apps don't use this directly; use @embedworkflow/react or /vue. Reach for embed-core only to build a wrapper for a framework we don't ship (Svelte, Angular, vanilla JS).

1
import { load, whenReady, mount, unmount } from "@embedworkflow/embed-core";
  • load(pk, { jwt }): resilient EWF.load (retries through the loader startup).
  • whenReady() / isReady(): resolve or return once window.EWF.mount exists.
  • mount(el): mount an element that already has an EWF__* class and data-* attributes.
  • unmount(el): tear it down (on component destroy).

Minimal custom-wrapper pattern:

1
2
3
4
5
6
7
8
9
10
11
const el = document.createElement("div");
el.className = "EWF__field";
el.dataset.workflowId = "wf_123";
el.dataset.fieldId = "slack";
container.appendChild(el);

whenReady().then(() => {
  el.addEventListener("ewf:change", (e) => console.log(e.detail));
  mount(el);
});
// on teardown: unmount(el)

window.EWF.mount exists only after load() runs, so its presence doubles as a readiness signal, which is what whenReady() polls for.

Gotchas

  • load() must run before anything mounts. A whenReady timeout means it wasn't called or the JWT was rejected.
  • Sign JWTs server-side. Never expose the secret in the browser.
  • New users need discover: true in the JWT, or the API returns 401.
  • data-base-path / basePath takes no leading slash. The renderer adds it.
  • Give the app an explicit non-% height and a catch-all route under its base path.
  • data-field-id is the field's name or id. Prefer the name. ewf:change reports the name you embedded with.
  • SPA route changes: the one-time declarative scan won't catch elements added after load(). Use a framework package or the imperative API.