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).
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 2const { pkToken, jwt } = await fetch("/api/ewf/embed-token").then((r) => r.json()); window.EWF.load(pkToken, { jwt });
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.
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 with | any stack | React or Vue apps |
| How it mounts | EWF.load() scans the DOM for EWF__* classes | components mount via window.EWF.mount |
| Best for | static markup, quick embeds | apps 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>
| Attribute | Applies to | Meaning |
|---|---|---|
data-workflow-id / data-workflow-key | settings-form, field | Which workflow |
data-field-id | field | Field name or id |
data-value | field | Initial value (uncontrolled) |
data-base-path | app | Base route (no leading slash) |
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-pathtakes 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 5interface 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).
1import { load, whenReady, mount, unmount } from "@embedworkflow/embed-core";
load(pk, { jwt }): resilientEWF.load(retries through the loader startup).whenReady()/isReady(): resolve or return oncewindow.EWF.mountexists.mount(el): mount an element that already has anEWF__*class anddata-*attributes.unmount(el): tear it down (on component destroy).
Minimal custom-wrapper pattern:
1 2 3 4 5 6 7 8 9 10 11const 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. AwhenReadytimeout 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: truein the JWT, or the API returns 401. data-base-path/basePathtakes 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-idis the field's name or id. Prefer the name.ewf:changereports 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.
Links
- React: the
@embedworkflow/reactpackage - Vue: the
@embedworkflow/vuepackage @embedworkflow/embed-coreon npm
