morphyDocs

Build your own cards

Write a card yourself, or connect your own tools to one: the files, the rules and the messages a card can send to Morphy.

Most people never write a card: they ask for one and an agent builds it. This page is for when you want to write one yourself, or connect a tool of your own to the canvas. It's what Morphy's agents follow too. To start chats and send them work from a program outside Morphy, see Drive Morphy from your tools.

A card is one HTML file

A card is a self-contained .html file in the cards folder of Morphy's data folder (%APPDATA%\Morphy\cards on Windows, ~/Library/Application Support/Morphy/cards on a Mac, see Your files), plus an entry in cards/canvas.json:

{
  "cards": {
    "habit-tracker.html": { "w": 460, "h": 380, "open": true }
  }
}
  • w and h are its size in canvas pixels. Leave out x and y: Morphy places a new card in a free spot beside its chat.
  • Re-read canvas.json right before you change it, and change only your card's entry. Morphy and its agents write the file too.
  • Both files are watched. Save the card or the layout and the canvas shows it at once.
  • The page's <title> is the card's name in its title bar.

The quickest start: ask Morphy for a card close to what you want, then open its file and change it.

What a card can and can't do

Every card runs walled off from Morphy, on an origin of its own:

  • It can't use Morphy's functions, read your files or chats, or see your keys. It talks to Morphy only through the messages below.
  • It can load scripts, styles and data from the web over https, and talk to a server on your own computer (http://localhost). That's the simplest way to connect a tool you run yourself. A card from the card library is the exception: it stays offline until it's changed.
  • Links open in your normal browser, web and email links only.
  • localStorage works, but it's for small things like a chosen tab. Anything you'd hate to lose goes in its saved data (below).

Write it so it holds up on a canvas:

  • No viewport units, no height: 100% layouts, no width media queries. A card is laid out again at a different size every time the canvas zooms, so a layout that depends on the window's size jumps around. Use plain document flow.
  • Put your scripts in try/catch. A card that breaks should still show something, not a blank page.
  • Never put a key or a password in a card. Anything a card holds, it could send anywhere. Keys belong in a script that runs outside the card (see Fresh data).

Talking to Morphy

A card sends a message to the page around it, and the answer comes back as a message with the same requestId:

function ask(message) {
  return new Promise((resolve) => {
    const requestId = Math.random().toString(36).slice(2);
    addEventListener('message', function answer(e) {
      if (e.data?.requestId !== requestId || e.data.type === 'morph.refresh.ack') return;
      removeEventListener('message', answer);
      resolve(e.data);
    });
    parent.postMessage({ ...message, requestId }, '*');
  });
}

Morphy always knows which card sent a message, so a card can only ever reach its own data and its own script.

Saved data

Each card has its own store: one JSON file in the card-data folder of Morphy's data folder (card-data/habit-tracker.json for habit-tracker.html).

const { ok, data } = await ask({ type: 'morph.data.get' });
await ask({ type: 'morph.data.set', data: { habits, ticked } });

The answer is a morph.data.result: { ok, data } for a read, { ok } for a save. Read the data when the card loads: when something else changes the file (a script of yours, or Morphy when you say "add milk to my list"), the open card loads again with it. Morphy can read the file too, to answer questions like "how many hours did I log in July?"

Sending to a chat

parent.postMessage({ type: 'morph.send', text: 'Summarise this week in my journal card' }, '*');
parent.postMessage({ type: 'morph.fill', text: 'Draft a reply to ' }, '*');
  • morph.send sends the text to Morphy's chat, as if you'd typed it. In a flow, it goes along the card's arrows instead.
  • morph.fill only puts the text in Morphy's text box, for you to read and send yourself. Use it for anything that matters.

Fresh data from a script

A card that shows live data (sales, a repository, your calendar) gets it from a script outside the card. The script can use keys and any tool on your computer. It fetches the data and writes it into the card's page or its saved data, and the open card shows it.

To give the card a ⟳ button that runs it, list the script in .morph/card-generators.json, with paths from the data folder:

{
  "sales.html": { "script": "assistant/scripts/sales.mjs", "args": [], "label": "Sales" }
}

Then from the card:

const result = await ask({ type: 'morph.refresh' });  // { ok, error }

Morphy answers morph.refresh.ack at once, runs the script with Node from the data folder (for up to 2 minutes), then answers morph.refresh.result. Only a script inside the data folder can be listed, and a card can only run its own. The script's keys can go in .morph/card-env.json ({ "sales.html": { "SHOP_KEY": "…" } }): they're handed to that card's script only, never to the card.

A PDF

const result = await ask({ type: 'morph.pdf', html: invoiceHtml, fileName: 'invoice-0042.pdf' });

Morphy prints the HTML as a vector A4 PDF and asks you where to save it. The answer is morph.pdf.result with { ok, file }, or { ok: false, canceled: true }. Give it a whole document with its styles inline. Its scripts don't run.

Following the theme

Morphy hands every card its colours as CSS variables, and changes them live with your theme and accent from Personalization. Use them with Morphy's dark values as the fallback, and the card looks the same outside Morphy:

:root {
  color-scheme: light dark;
  --bg-0: var(--morph-bg, #1c1d20);
  --ink: var(--morph-ink, #f3f3f1);
  --ink-2: var(--morph-ink-2, #a4a6aa);
  --accent: var(--morph-accent, #c3caff);
  --accent-fill: var(--morph-accent-fill, #3446f0);
}

The full set is --morph-bg, -bg-2, -ink, -ink-2, -ink-3, -accent, -accent-fill, -accent-mid, -series-2, -on-accent and -danger, plus --morph-ink-rgb and --morph-accent-rgb for tints (rgb(var(--morph-ink-rgb) / .07)). A chart drawn in a <canvas> reads them with getComputedStyle and draws again on the morph:palette event.

Numbers Morphy can read

A card that shows numbers can also state them, so Morphy reads them instead of guessing from the page. Put one block in its <head>, written from the same values the card shows:

<script type="application/json" id="morphy-facts">
{"updated":"2026-10-25T09:00:00Z","facts":[
  {"label":"Sign-ups today","value":13},
  {"label":"MRR","value":205,"unit":"$"}
]}
</script>

Share it

Made one others would like? Right-click its title bar → Share with the community…. Good ones go in the card library.

On this page