For AI coding agents A human doing the integration? Developer docs →

Integrate Morph Embed: agent guide

This page is written for an AI coding agent (Claude Code, Cursor, Copilot, Cline, etc.) tasked with wiring the morph-embed SDK into a web app. It is terse, imperative, and copy-paste ready. Follow it exactly. If you are pointing an agent at Morph, hand it the raw version below.

Raw text for your agent → /llms.txt SDK · morph-embed@0.3

Your goal

Add Morph Embed to the target web app so its end-users can reshape the UI and generate real-data widgets by prompt. A correct integration is: (1) the SDK loads, (2) Morph.init is called exactly once with a valid publishable key, and (3) the app's real data/actions are registered as sources/actions so widgets show live values, not placeholders.

Start here

Do the minimal thing first (script tag + init with just a key → reshape works), confirm it, then add sources/actions. Don't block or wrap the host app's render on Morph; it is strictly additive and must fail open.

Minimal integration

index.html html
<script src="https://unpkg.com/morph-embed@0.3/dist/morph-embed.js"></script>
<script>
  Morph.init({ key: 'pk_live_…' });   // reshape works immediately; add sources next
</script>

Install

Pick the form that matches the target project.

  • Script tag (static site / server-rendered HTML) adds window.Morph. Pin a major: morph-embed@0.3.
  • npm (bundled app): npm install morph-embed, then import { init } from 'morph-embed' (ESM). The package default export path is the ESM build; morph-embed/iife is the browser-global build.

The publishable key

  • The key is pk_live_… and is publishable: it is designed to appear in client source. It is NOT a secret. In a bundler, expose it as a public env var (NEXT_PUBLIC_MORPH_KEY, VITE_MORPH_KEY, etc.), not a server secret.
  • Get one at usemorph.xyz/dashboard. If you cannot obtain one, leave a clearly-marked 'pk_live_REPLACE_ME' placeholder and tell the human to create a key and register the app's exact origin(s) (scheme://host[:port]; www and apex are distinct).
  • A request from an unregistered origin returns 403; a bad key returns 401. If reshape fails with 403, the fix is registering the origin, not changing code.

Config schema

Morph.init(config) is the full contract. Only key is required.

morph.config.ts ts
type MorphConfig = {
  key: string;                                // pk_live_… (required, publishable)

  sources?: Record<string, {
    fetch: (query) => Promise<object[]>;         // return the app's records; runs in-page under app auth
    schema: Record<string, 'string'|'number'|'date'|'boolean'>;
    description: string;                        // one line; enum values help
  }>;

  actions?: Record<string, {
    run: (args) => Promise<unknown>;             // your callback; args validated first
    argsSchema: Record<string, 'string'|'number'>;
    description: string;
    confirm?: boolean;                          // default true; keep true for destructive actions
  }>;

  storage?: {                                   // omit → localStorage (per-browser)
    get:    (key: string) => unknown | Promise<unknown>;
    set:    (key: string, value: unknown) => void | Promise<void>;
    remove: (key: string) => void | Promise<void>;
  };

  trigger?: 'alt+m' | 'ctrl+shift+k' | 'cmd+k' | 'none';   // default 'alt+m'
  launcher?: false | { position?: 'bottom-right'|'bottom-left'; accent?: string };
  backend?: string;                            // override API endpoint (rarely)
};

After init, window.Morph.open() opens the bar (pair with trigger:'none' and your own button); window.Morph.bar has .show()/.hide()/.toggle().

Wire the app's data as sources

This is the high-value step. For each dataset the app already has, register a source whose fetch calls the app's existing API/state and returns an array of plain objects, and whose schema declares every field a widget might reference.

sources js
sources: {
  orders: {
    fetch: async () => (await api.get('/orders')).data,   // reuse the app's real client
    schema: { status: 'string', total: 'number', createdAt: 'date' },
    description: 'All orders (status: paid|refunded|pending)',
  },
}
Warning

Only register sources backed by real, fetchable data. Never invent a source or a field. Every field named in schema must actually exist on the returned objects, and any field a user might ask about must be in the schema. A missing field is a hard error at query time, not a silent zero. Dates may be ISO strings, epoch ms, or Date.

Wire actions (optional)

Register an action only if the app should let users trigger a real mutation from a generated button.

actions js
actions: {
  createTask: {
    run: async (args) => api.post('/tasks', args),
    argsSchema: { note: 'string' },              // string|number only
    description: 'Create a follow-up task',
    confirm: true,
  },
}

The generated button is inert until a real user clicks it, and (with confirm:true) shows a confirm dialog first. Args are schema-validated; unknown/off-type args are dropped.

Storage (optional)

Default is localStorage (per-browser). To sync a user's generated features across devices, pass a {get,set,remove} adapter backed by the app's own persistence, keyed by the current user. Values are JSON-serializable; methods may be async; failures must not throw.

Recipe · plain HTML

index.html html
<script src="https://unpkg.com/morph-embed@0.3/dist/morph-embed.js"></script>
<script>
  Morph.init({
    key: 'pk_live_…',
    sources: { orders: { fetch: () => window.__ORDERS__, schema: { total: 'number', createdAt: 'date' }, description: 'Orders' } },
  });
</script>

Recipe · React

Init once, after mount. Guard against double-init (React 18 StrictMode / HMR double-invoke).

MorphProvider.jsx jsx
import { useEffect } from 'react';
import { init } from 'morph-embed';

function MorphProvider() {
  useEffect(() => {
    if (window.__morphInit) return;      // idempotent guard
    window.__morphInit = true;
    init({
      key: process.env.NEXT_PUBLIC_MORPH_KEY,
      sources: {
        deals: {
          fetch: async () => api.deals(),
          schema: { stage: 'string', amount: 'number', closedAt: 'date' },
          description: 'CRM deals (stage: won|lost|open)',
        },
      },
    });
  }, []);
  return null;
}

Render <MorphProvider/> once, high in the tree, in a client component.

Recipe · Next.js (App Router)

  1. Create a client component ('use client') identical to the React recipe above and render it once in app/layout.tsx.
  2. Put the key in .env.local as NEXT_PUBLIC_MORPH_KEY=pk_live_… (public by design; do not use a server-only var).
  3. Do NOT import morph-embed in a Server Component; it touches window. Keep it in a 'use client' module inside useEffect.

Rules: do / don't

Do Don't
Call init exactly once; guard against re-init on HMR/StrictModeCall init per render or per route change
Put the pk_live_ key in a PUBLIC env var / client sourceHide it as a server secret or proxy it (it's publishable by design)
Register only sources backed by real fetchable dataInvent sources, fields, or numbers
Declare every queryable field in schemaOmit fields users might ask about (hard error at query time)
Keep confirm:true for anything destructiveSet confirm:false on mutations that can't be undone
Let Morph load async and fail openBlock the app's render on Morph, or throw if it 4xxs
Load morph-embed only client-sideImport it in a Server Component / SSR path (uses window)
Tell the human to register the app's origin in the dashboardAssume a 403 is a code bug (it's an origin-allowlist gap)

Verification checklist

After integrating, confirm each of these (in a browser at a registered origin):

  • The SDK loaded: typeof window.Morph.init === 'function'.
  • init ran once (no duplicate launcher/bar in the DOM).
  • Pressing Alt+M or clicking the launcher opens the prompt bar.
  • A reshape prompt ("make this page dark") applies and shows an Undo toast.
  • If sources are registered: a data prompt ("total <metric> this month") renders a real number, not a placeholder.
  • Reloading the page reapplies the saved edit.
  • No console errors from the host app; Morph failing to load does not break the page.
If it fails

If a data widget shows a placeholder instead of a number, the source isn't registered or a query field isn't in its schema. Fix the source/schema; do not hardcode a value.

How it works (context you may need)

The model never returns code. It answers only in a fixed declarative JSON DSL that the SDK validates and sanitizes in the browser before touching the DOM; your data never leaves the page (only source names and field types are sent to the model). You don't write the DSL, but if you're debugging what a prompt produced, the op grammar the model emits is documented for reference in the developer docs. Everything else an integrator needs is on this page and in the developer docs.

Links

Package: morph-embed · keys: dashboard · raw guide for your agent: /llms.txt.