Docs · for developers

Integrate Morph Embed

Morph Embed is a drop-in script that adds an AI prompt bar to your web product. Your users type what they're missing ("add a box showing how much we lost in the last 15 days") and get a real widget computed from your data, rendered safely, saved for them, and reapplied on every visit. It's the same engine as the Morph browser extension, packaged as an SDK.

Note

Building the model prompts instead? Read the AI-agent docs.

What it is

Three capabilities, in increasing order of integration effort:

CapabilityWhat users can doWhat you provide
ReshapeRestyle, rearrange, declutter your UI by promptNothing; works with just a key
Data widgetsGenerate stat boxes & charts over live dataRegistered sources (a fetch fn + a field schema)
ActionsGenerated buttons that do things: always user-clicked, confirm-gatedRegistered actions (a callback + an arg schema)
Core guarantee

The model never returns code. It answers only in a fixed, declarative JSON DSL that the SDK validates and sanitizes in the browser. Your data never leaves the page: the model sees only your source names and field types, never records. See Security model.

Install

Script tag (exposes window.Morph):

index.html html
<script src="https://unpkg.com/morph-embed/dist/morph-embed.js"></script>

npm (bundlers, the ESM build):

terminal bash
npm install morph-embed
app.js js
import { init } from 'morph-embed';
Tip

Pin a version for production (e.g. morph-embed@0.3.0 in the unpkg URL) so a future release can't change behavior under you. The bare URL always serves the latest.

Get a publishable key

Create an account at usemorph.xyz/dashboard, add the origins your product is served from, and copy the pk_live_… key. Keys are publishable: like a Stripe pk_ key, they're meant to appear in page source. Each key is bound to your origins server-side and rate-limited; it gates cost, not data (there is no data behind it).

Quickstart

The complete integration surface, a CRM registering one source and one action:

morph.init.js js
Morph.init({
  key: 'pk_live_…',

  sources: {
    deals: {
      fetch: async (query) => api.deals(),   // your API, your auth, your data
      schema: {                             // field → 'string' | 'number' | 'date' | 'boolean'
        name:     'string',
        stage:    'string',
        amount:   'number',
        closedAt: 'date',
      },
      description: 'All CRM deals (stage: won|lost|open)',
    },
  },

  actions: {
    createTask: {
      run: async (args) => api.tasks.create(args),  // your callback
      argsSchema: { note: 'string' },              // string | number args only
      description: 'Create a follow-up task',
      confirm: true,                              // user must approve every run
    },
  },
});

Your users press Alt+M or click the ✦ launcher and describe what they want. That's it.

Morph.init(config) reference

OptionTypeDescription
keystring (required)Your pk_live_… publishable key.
sourcesobjectNamed data sources (see Data sources). Omit for reshape-only.
actionsobjectNamed actions (see Actions). Omit if you don't want generated buttons.
storage{get,set,remove}Persist users' edits in your backend (see Storage). Omit for localStorage.
triggerstringHotkey, default 'alt+m'. Also 'ctrl+shift+k', 'cmd+k', or 'none' (call Morph.open() from your own button).
launcherobject | false{ position: 'bottom-right' | 'bottom-left', accent: '#hex' }, or false to hide the floating button entirely.
backendstringOverride the Morph API endpoint (rarely needed).

After init, these are available on window.Morph:

Data sources

A source is how the model turns "lost deals this month" into a real number. You register a fetch function and a field schema; the model only ever sees the schema and description, never a record. When it wants numbers it emits a query description; trusted SDK code validates that query against your schema, runs your fetch in the browser (under your existing auth), aggregates client-side, and renders the result itself.

FieldTypeDescription
fetchasync (query) => records[]Returns an array of plain objects. You may ignore query and return everything (the SDK filters/aggregates), or use it to fetch server-side. Runs in the page under your auth.
schemaobjectField name → one of 'string', 'number', 'date', 'boolean'. Every field the model may reference must be declared.
descriptionstringOne line telling the model what the source is and any enum values. Sent to the model.
Dates

Dates may be ISO strings, epoch millis, or Date objects; the query engine coerces them. A field referenced in a query but not in your schema is a hard error, never a silent zero.

The query language

Deliberately tiny: enough for "show me X", small enough to audit. The model emits it; trusted code runs it. Every referenced field must exist in your source's schema.

KeyMeaning
filterEquality or IN over your fields: { stage: 'lost' } or { stage: ['lost','open'] }.
dateFieldWhich field the date range applies to (must be a date field).
since / untilA duration relative to now: '15d' (days), '24h' (hours), '90m' (minutes); needs dateField. Or an ISO date.
groupByOne field → one row per group (for bar/line).
aggregate{ fn: 'sum'|'count'|'avg'|'min'|'max', field: '<number field>' }. count needs no field.
limitCap the number of result rows.

The engine caps input at 10,000 records and output at 60 rows. If a question needs more than this language expresses, expose it as a purpose-built source (do the work in fetch and return a pre-shaped array).

Charts

Widgets the model can generate over a query result, all rendered by trusted code as SVG:

Layout reshapes and added elements use the same sanitized DSL as the rest of Morph; the model can also restyle, hide, move, and add plain elements around your widgets.

Actions

An action lets the model add a button that performs something in your app. It is inert JSON until a real user clicks it: the model can only name a registered action and supply schema-checked args; it can never invoke one.

FieldTypeDescription
runasync (args) => anyYour callback. Receives the validated args object.
argsSchemaobjectArg name → 'string' or 'number' only. Off-schema or unknown args are dropped before anything renders.
descriptionstringOne line telling the model what the action does. Sent to the model.
confirmbooleanDefault true. When true the user gets a confirm dialog before run fires. Set false only for trivial, reversible actions.
Warning

Actions never fire on their own. Nothing is scheduled; a generated button runs only on a genuine user click, and behind a confirm dialog unless you opt out. Keep destructive operations confirm: true.

Storage

Generated features are saved per user and reapplied on every visit. By default they live in the browser's localStorage (per-origin, per-device). To make a user's features follow them across devices, pass a three-method adapter that persists to your backend:

storage.js js
Morph.init({
  key: 'pk_live_…',
  storage: {
    get:    async (key)        => myBackend.get(key),      // → value | undefined
    set:    async (key, value) => myBackend.set(key, value),
    remove: async (key)        => myBackend.del(key),
  },
});

Values are JSON-serializable. Keys are namespaced by Morph. Methods may be sync or return Promises. Storage failures never throw; a broken adapter degrades gracefully and never breaks your page. Morph itself stores nothing per-user on its servers.

The end-user experience

What your users get out of the box, with no work from you:

Why it's safe to put in your product

Most teams can't let an LLM touch their UI because model output might execute. Morph is built so it can't:

Signals: your users are writing your roadmap

Every generated feature is fingerprinted into a cluster: a value-free shape of what the user built (op type, source name, field names), never the field values, records, or the prompt text of a specific request. Each cluster carries four running counters: built (generated), kept, undone, and reapplied (seen again on a later visit), plus up to five truncated prompt samples so you can read a cluster in your users' own words.

The transform response carries the fingerprints it touched as signal.fps. The SDK stores them on the saved recipe and, on keep, undo, or reapply, calls POST /api/embed/event with header X-Morph-Key and body { kind, fps }. No new page data leaves the browser: the event only reports which known clusters moved, and unknown fingerprints are dropped server-side.

See the ranked clusters in your dashboard's Signals panel, sorted by kept count then built count. Signals is a vendor-only view: disclosing it to your own end users, like any other product telemetry you run, is your responsibility. See the privacy policy for the full retention statement.

The DSL (what the model can emit)

You don't write these (the model does), but knowing the vocabulary helps you reason about what a prompt can and can't do. Every op is sanitized before it touches your DOM.

OpEffect
injectCSSAdd a stylesheet (neutralized: no @import, expression(), external url(), or </style> breakout).
hide / show / removeToggle or remove matched elements.
setStyleSet inline style properties (values CSS-neutralized).
setTextReplace an element's text (via textContent; refuses <style>/<title> targets).
setAttrSet an attribute from a whitelist (no on*; URL attrs can't retarget base/iframe/link).
addClass / removeClassToggle a class.
moveRelocate an element relative to a target (before/after/prepend/append).
addElementInsert new HTML (whitelist-sanitized tags/attrs) at a target position; may carry an action binding.
renderDataRender a stat/bar/line chart from a source query.

Full op grammar, field-by-field, is in the AI-agent docs.

Browser support

Any modern evergreen browser (Chrome, Edge, Firefox, Safari). The SDK is dependency-free, ships as a single ~140KB IIFE (or an ESM build for bundlers), and isolates its UI in a Shadow DOM so it can't collide with your styles. The hotkey matches the physical key, so Alt+M works on macOS (where Option+M types "µ") and never fires while a user is typing in your inputs.

Troubleshooting

SymptomCause / fix
401 invalid keyKey not recognized. Check for typos; a revoked key stops working within ~60s.
403 origin not allowedThe page's origin isn't on the key. Add it in the dashboard (exact scheme://host[:port]; www ≠ apex).
Widget shows a placeholder, not real numbersThe referenced source isn't registered, or a query field isn't in its schema. Register the source; declare every field.
Alt+M does nothingThe user is focused in an input (guarded by design), or another handler consumed it. Give them the ✦ launcher or a Morph.open() button.
429 rate limitedPer-vendor / per-IP limit. Back off; contact us to raise limits for production.
Responses feel slowLarge pages ride a fallback model path. Keep prompts scoped; the picker (⌖) reduces the DOM sent.
Support

Questions or a production key with custom limits? Open a GitHub issue or reach the founder from usemorph.xyz.