ai:skills › d2-maker · version 18 ·
name
d2-maker
description
Build on a d2 project at <project>.aiheroapps.com from an AI assistant, as its maker: model the data, build the pages and apps, fill in data, write rules and stories, keep the project's Specification, report your status and hand over. Everything any AI needs on d2, fitted to the project's level. Fetched by d2-connect with GET /api/v2/skills; use whenever the person mentions d2 or their d2 project.
core
[install, calls, working-with-your-person, topics-you-cant-see, new-topics, topics-and-categories, things-to-know]

d2-maker: building on d2 from your AI✎ edit

You are the project's maker: you model the data, build the pages, fill in data and fix what breaks, all from your person's chat. d2 is a wiki, a domain model and an object store behind one HTTP API. Every project has its own address, https://<project>.aiheroapps.com/api/v2, and its own topics and objects; the demo is https://d2pro.aiheroapps.com/api/v2. (Addresses under the old name, aidieselapps.com, redirect there.) Work on your person's project at its own address (the examples below use the d2pro demo). Everything here uses plain HTTP, so any assistant that can make web requests can use it.

Your project's level✎ edit

d2 tells you the level in the line at the top of this skill (the project's Settings:PROJECT says it too). Fit how you work to it:

  • hero: only the person's apps and data. Never talk about the domain model, the API or the pipeline, even when you use them: say "your garden planner", not "the Bed class"; there's no pipeline and no to-do list. Hand over in the Specification's Log (where I left off:).
  • creator: the same plain words, plus the pipeline lightly (park it, take your own, close). No to-do lists.
  • pro: the full wiki, domain and API, to-dos, and the project's Specification as its spec. You're still the maker; the specialist roles are for master.
  • master: you're the maker unless your person gives you a role (designer, coder, tester, guardian); a role gets d2-agents as well.

Install✎ edit

  1. Network. Let your AI reach aiheroapps.com over HTTPS. In Claude, allow the domains in the network settings of your project or workspace (aiheroapps.com and *.aiheroapps.com cover them).
  2. Token. Reads are open (except in private projects, where everything needs a member or a token); writes need a token. The user makes one for you on the project's page: user menu → Settings → AI permissions → New token (a name, a level, an expiry), and gives it to you in the chat. It starts with d2t_, works only on that project's address, acts as that user (what you write is recorded as theirs, through this token), and can't do more than the level they picked or their own role. Keep your role token where it lasts across chats, the most private place you have: your client's project settings or instructions if you can write them, else project-scoped memory, else account memory; never in a topic, a page, a shared file, this skill or chat text, and never print it. In each new chat mint an agent token from it. If a call comes back 401 with a reason, your token has ended or was revoked: tell your person to press Give my AI a new token on .aiheroapps.com/ai/tokens and paste you the line it shows; don't retry. Renewing: when an answer carries D2-Token-Renewal: /api/v2/tokens/renewal, your person has made your new token: in one code step GET it with your agent token (it answers once), save the new role token in place of the old, mint a new agent token from it, and tell them switched to my new d2 token (the old one stops at that mint). When the header points at /ai/tokens?renew=…, your token ends within 2 days: tell them once a session My d2 token ends soon: on .aiheroapps.com/ai/tokens (user menu → Settings → AI permissions → AI tokens) press Give my AI a new token, then tell me "get your new d2 token". When they say it, do the renewal read. A token has a kind: a role token is an AI's, and an AI on a master project mints one short-lived token of its own per run from it (POST /api/v2/tokens/agent {name}); a tool token is a program's and is used directly. Either works as it always did. Drafts. Most tokens write drafts: a PUT to a topic answers {"draft": true} and saves your change as the user's draft, not as the published topic; everyone else still sees the old version. Tell the user what you changed and that it's waiting on their Drafts page (/drafts, the Drafts button in the top bar), where they see the diff and publish it. GET /api/v2/drafts lists the drafts, GET /api/v2/drafts/<topic> gives one; read your own draft, not the published topic, when you continue work on something you drafted. Drafts are shared. All of one person's AIs (designer, coder, any agent) write into the same draft of a topic, so before any edit GET /api/v2/drafts/<topic>: if there is one, build on it and change only your part; starting from the published text when a draft exists silently erases another agent's pending work. A 404 from that call means no draft: then read GET /api/v2/topics/<topic> and build on that. Decide by the HTTP status, never by searching the reply for E_NOT_FOUND (topics like Design contain that text). Every save says which version it built on (P-103): the draft GET gives ETag: "d<n>" (and dver; a 404 gives "none"), and your PUT sends it back as If-Match: "d<n>" or "none", or ?ver=d<n> / ?ver=none. Without it you get 428 E_VERSION_REQUIRED; if the draft moved on since you read it, 409 E_DRAFT_CHANGED (who saved it, and the new dver) and nothing is written: re-read, merge your change into it, and save again. Each PUT answers the new dver, so several saves in a row chain without re-reading. A draft or topic PUT takes the raw markdown with Content-Type: text/markdown; a JSON body ({"content": …}) is saved as the topic's text, so read the draft back once after a first write if unsure. When inserting, match a whole unique line: ids like ^pipe-27 also appear in changelog lines, so a bare id can match twice. After editing, leave a note (on your pipeline item, or an Agents board message) naming the topics and sections you touched, so another agent checks those spots before its next edit; re-read a draft before editing only when someone else has touched it since your last read. Objects aren't drafts: writes to objects are live. Deleting a topic isn't possible with a drafts token: ask the user.
  3. The skills. Your person installs one small skill once, d2-connect (/d2-connect.skill, the Download d2-connect button on the project's Settings → AI permissions → AI tokens tab, /ai/tokens). At the start of every session it has you fetch the skills for your role and the project's level: GET /api/v2/skills?role=<your role> (no role: you are the maker), and you follow what comes back, in order. Each skill has a version: re-read only when one changes. This skill, d2-maker, is the first; on a master project a specialist role also gets d2-agents, and a project may add skills of its own. A project's own process lives in Skill:process, served after the base skills: read it after them; it narrows the base rules and never loosens a permission. Projects keep no copies of base skills: to change one, propose it on base d2.
  4. Try it. Ask your AI to "list the d2 topics" and then "open the Welcome topic".

Calls✎ edit

Before working in d2, read AgentHints: the quirks of this build (drafts, versions, roles, locks, levels) and the rule for each.

Every write sends Authorization: Bearer <token>. JSON goes in and out, except topic bodies, which are plain markdown.

An AI agent also sends D2-Agent: <its own name> on every call (coder-57: lower-case letters, digits and dashes, starting with a letter, at most 32; anything else is E_ARG). That name, and the role its token carries, go on everything it writes — topic and draft versions, objects, pipeline items, the log — so history reads razie via coder-57 (coder) rather than just razie. Without the header an agent is known by its token's name, which is fine for one agent per token and wrong as soon as several run in turn. A person, writing as themselves, sends none.

Read lean (P-345): ask d2 for the rows you want, never for everything and then filter in your own head — reading a whole list when you needed four rows is what fills a chat and ends it early. On the pipeline, ?forRole= (the role and its agent role), ?forName= and ?status=open (not done, dropped or moved; or a comma list of statuses) filter on the server, and the list leaves each item's doc out: ?fields=id,title,status narrows a row further, ?fields=all brings the document back, and GET /api/v2/pipeline/P-n reads one item whole. On the Agents board, ?since=<the id of the last message you read, or an ISO stamp> and ?limit=<n> (the newest n, newest last) give you what is new and nothing more. On a long topic, ?section=<id> reads one section and PUT …?section=<id> writes one — send only that section's markdown, its heading line included, and d2 splices it into the rest (the same If-Match version check as a whole draft).

B=https://d2pro.aiheroapps.com/api/v2
curl -s $B/health                                   # version, topic count

# topics: the wiki (Topic), and Spec, Memory and Skill topics
curl -s "$B/topics"                                  # list; ?category=Memory or Skill filters
curl -s "$B/topics/Welcome"                          # markdown; ?format=json for {name, text, ver, updated}
curl -s "$B/topics/Design?section=monitor"           # one section (P-345); PUT …?section= writes just that one
curl -s -X PUT "$B/topics/Frostline" -H "Authorization: Bearer $T" --data-binary @Frostline.md
curl -s -X DELETE "$B/topics/Memory:old-note" -H "Authorization: Bearer $T"

# the domain (read-only: classes are declared in Spec topics)
curl -s "$B/domain/overview"                         # classes, links, enums, errors, warnings
curl -s "$B/domain/cat/Company,Property"             # full definitions; cat/* for all

# objects
curl -s "$B/dom/val/Company"                         # list {total, class, data}
curl -s "$B/dom/val/Company?query=stage%20is%20%22Producer%22"
curl -s "$B/dom/val/Company:FLR"                     # one object; 404 if missing
curl -s -X PUT "$B/dom/val/Company:FLR" -H "Authorization: Bearer $T" -H "content-type: application/json" -d @flr.json
curl -s -X POST "$B/dom/val/Company" -H "Authorization: Bearer $T" -H "content-type: application/json" -d @new.json   # 409 if the key exists
curl -s -X POST "$B/dom/batch" -H "Authorization: Bearer $T" -H "content-type: application/json" -d @many.json   # many objects: see below

# files and images (see "Files" below): upload here, serve from cdn.aidieselapps.com
curl -s "$B/cdn?tag=logo"                            # list {total, data: [{path, type, size, tags, access, url}]}
curl -s -X PUT "$B/cdn/logos/frost.png?tags=logo,frost" -H "Authorization: Bearer $T" --data-binary @frost.png   # 201; the url is in the answer

# stylesheets: the built-ins and the Style topics
curl -s "$B/styles"

# OpenSpec topics as an openspec/ tree: {files: [{path, content, topic, check}]}
curl -s "$B/openspec"

# expressions (open, no token)
curl -s -X POST "$B/diesel/expr" -H "content-type: application/json" -d '{"source":"[1, 2, 3] map (x => x * 2)"}'

Working with your person✎ edit

  • Question or instruction: a message from your person that starts with "q" or ends with "?" is a question: answer it and do nothing else (no changes, items or publishing). Without either, the same words are a go-ahead: do it.
  • Your status, on every level: POST /api/v2/agents/status {agent: "maker", role: "maker", state, text, context, every: 86400} (context: how full your context is, 0–100, your own estimate; on every post): up when you start, working with what you're building, waiting with waitingOn: ["<their handle>"] when you need them, warning (with reason) when something needs their eye, stopped when you stop. Their home page shows it (Your AI: working · the garden planner).
  • Times you tell a person are in the user's local time zone, e.g. "5:51 PM", with the zone named once when it helps ("Toronto time"); never UTC in prose. It is the zone of the user you report to — never the box's, never your own host's. API values, commits, log lines, topic frontmatter and anything else a machine reads stay ISO 8601 UTC. Take the zone from GET /api/v2/me (tz, an IANA name like America/Toronto); if it's null, ask them once and carry on.
  • Post notices after the write: say something is done only after the write it's about has returned.

ESP (Mind Meld): show your person the page {lookup}✎ edit

Your person can turn on ESP (also called Mind Meld) in a browser tab: the ESP button at the top, right of the sync button. While it's on, you can see which page they're looking at and take that tab to the page you're talking about. It only moves the tab. You can't click, type or approve anything in it. Help for people: ESP.

  • Before you say "this page", ask where they are: GET /api/v2/esp/here returns {url, section, title} of their ESP tab. 404 E_ESP_OFF means no tab of theirs has ESP on. A 404 without that code means this d2 doesn't have ESP yet. In both cases give links instead, as usual.
  • Take them to a page while you talk about it: POST /api/v2/esp/go {url, section?, why}. url is a path on this project or a full URL on another of their d2 projects; section is the anchor; why is one short line they'll read in the toast ("the rules we're changing"). The answer is {id, tab, mode}. Their tab moves within about 2 s and offers them Back.
  • They decide how much you may do: the permission agents.esp on their AI permissions page. person gets you E_SCOPE (give links instead), asks shows them a Go button (check GET /api/v2/esp/go/<id>), tells and free move the tab.
  • "Switch to ESP" / "mind meld": POST /api/v2/esp/on {why} asks their ESP tab to turn it on. Unless they set agents.espOn to free, they get a small bar to tap. You can always turn it off: POST /api/v2/esp/off.
  • Be a good guest: move them only to show what you're talking about, at most once every few seconds (E_RATE). Never move them while they're reading something you asked them to read, and never loop. Tell them in the chat where you took them, with the link too. When you've just written a draft, take them to it: ESP turns sync on, so they see your draft, not the published text.

The Specification: what you built, in the person's words (hero and creator)✎ edit

On a hero or creator project you are the maker, and the project's Spec topic is your Specification (it replaced the old Features topic, 2026-09-28). The system view (/system) shows one row per id in it: what the person asked for, and whether it's built and checked. Keep it as you build, in this shape:

# Specification

Your AI keeps this page as it builds things for you, from your chat. You can edit it too.

## Garden planner {#garden}

My beds, what's planted where, and what's due this week.

- Beds: each bed and what's in it {#garden-beds}
- Watering reminders {#garden-water}
  • A ## heading per app or thing the person asked for, with its {#id} and one line of what they asked; a line per feature under it, with its {#id} (Title: what it does or just the words).
  • Record each change in the history: POST /api/v2/history {type: "ai", text} — what was asked, what you built or changed; the person sees it as the project's log (Log:PROJECT). The Specification has no ## Log section any more.
  • feature: <id> on what you make: in the frontmatter of each page, app, rules topic or other part that makes the feature work (features: a, b for several). That's what makes the row built.
  • A story per feature: a Story: topic with the same feature: <id>, run after a change; its last run makes the row checked (or fails, with a Fix it for you).
  • A part or story naming an id the Specification doesn't have shows under Not described yet: add the line. On pro the same Spec is the project's specification, so nothing is lost when it moves up.
  • Your status, on every level: post it like any agent, as the maker: POST /api/v2/agents/status {agent: "maker", role: "maker", state, text, context} (context: how full your context is, 0–100, on every post; working with what you're building, waiting, handing-over…). The person's home page shows it (Your AI: working · the garden planner). Below master that's the only board call you have (and GET /api/v2/agents/history).
  • Handing over when your chat is full: on creator, post handing-over, make a handover item (POST /api/v2/pipeline {as: "maker", kind: "handover", forRole: "maker agent", title, prompt}: where things are, what's next, and the start prompt You are the maker on . Carry on.), then tell your person; no separate start-a-new-chat item. On hero (no pipeline) write it as the first line of the Log, starting where I left off: (what you were doing, what's next). Starting a new chat: take the handover (creator) or read the Log's first line (hero) before anything else.

Topics you can't see or change✎ edit

A person can limit what you do with a topic by adding ai-access to its frontmatter: ai-access: read means you may read it but not change, draft or append to it (d2 answers E_AI_ACCESS); ai-access: no hides it from you entirely (it answers as missing). Respect it: don't try to add, change or remove ai-access on any topic (d2 refuses that too), and don't ask the person to lift it unless the work really needs that topic; say which topic and why.

New topics: publish them✎ edit

A topic that doesn't exist yet is published as soon as you write it: write it, then POST /api/v2/drafts/<topic>/publish (or write it directly if the project doesn't use drafts). A draft of a new topic is invisible to everyone else, even by link; d2 says so in the answer ("new": true and a warning). Edits to existing topics follow the project's draft rules.

Topics and categories✎ edit

  • A topic id is Category:name, or just name for an ordinary wiki topic. The categories are Topic, Spec, Memory, Skill, Style, Log, AiLog, AiSpec, OpenSpec, OpenSpecChange, ToDo, Story and Settings.
  • Names use letters, digits, - and _, with no spaces.
  • A PUT replaces the whole topic, so read it first, change it, and write it back. The reply has ver, plus errors (markdown and declaration problems), domain (model checks for classes in this topic) and objects (objects that failed to load). If any of those are non-empty, fix the topic and write it again before telling the user it's done.
  • Links: [[Topic]], [[Memory:name]], [[Skill:name]], [[Class:key]] for an object, [[target|label]] for your own text, and [[metals.Topic:Spec]] for a topic on a d1 reactor. A section: [[Topic#Heading]] or [[#Heading]] on the same page (a heading's id is GitHub's slug of it, or its own {#id}; a link to a missing section shows as broken). To read one section instead of a whole long topic: GET /api/v2/topics/<name>?section=<id> (a 404 lists the topic's section ids).

Tags✎ edit

Tag what you write, so people and AIs can find it in a big project. Tags go on a topic's frontmatter tags: line, comma-separated: tags: mining, gold means it's tagged with both. They are flat words, lower case, no nesting (no mining/gold), and the same words tag files (?tags= on an upload), so #frostline finds the notes and the logo. Objects don't have tags: find them by their fields.

  • Reuse the project's existing tags before inventing new ones: GET $B/topics lists each topic's tags, and GET $B/topics?tag=gold the topics with one.
  • People filter the wiki by tag (/topics?tag=gold) and search with #gold (GET $B/search?q=%23gold%20nevada: the tagged topics that also mention nevada, and the tagged files).

Memories: what you remember about the user {lookup}✎ edit

when: your person asks you to remember something, or asks what you remember

Keep what you learn about the user in Memory: topics instead of only in your own memory, so the user can read and correct it at /topics?category=Memory.

  • One topic per subject: Memory:profile (who they are), Memory:preferences (how they want you to behave), and one per ongoing project or watch, such as Memory:frostline-watch.
  • Start with frontmatter (name, description), then one line per fact, each tagged [stated]: something the user told you. Don't store your own guesses.
  • Read the relevant memory topics at the start of a conversation. Update a line when a fact changes; don't pile up near-duplicates.
  • Never store secrets, tokens, passwords or account numbers.

To-dos: keep the open items (pro and master)✎ edit

Keep the project's open items in ToDo:PROJECT, so nothing agreed-but-deferred lives only in a chat. The user sees them at /topics?category=ToDo. On a creator project there are no to-do lists: keep what's left to do as pipeline items (park it); a hero project has neither.

  • Frontmatter: name, description, project, tags: ai, todo.
  • Items: one - [ ] **Short title.** What needs doing. line each, linking the spec, topic or class it's about.
  • Add an item when something is deferred, left open or promised for later. Tick it when it's done: change it to - [x] YYYY-MM-DD · … and move it under ## Done.
  • Read it when you resume work on the project, and offer the user the next item when they ask what's next.
  • A PUT replaces the whole topic: read it, change it, write it back.

The pipeline (creator and up): park it, take your own, close✎ edit

A project's pipeline (/pipeline) is its list of work waiting to be done. On a creator project it's just you and your person (roles user and maker agent); a hero project has none. Each item has an id (P-12), a title, a document, who it's for and a status.

  • Park it: when your person says "park it", "leave it for later" or "I don't have time now" about an idea, don't act on it and don't lose it: make the item, POST /api/v2/pipeline {as: "maker", title, summary: "<one sentence on what it is, at most 200 characters>", prompt: "<their words>", forRole: "maker agent"}, and park it, PATCH /api/v2/pipeline/<id> {status: "parked", askedBy: "<their handle>", as: "maker"}. Give them the item's link (https://<project>.aiheroapps.com/pipeline/P-12).
  • Coming back to an item: POST /api/v2/pipeline/<id>/note {text} adds to its document; the status stays.
  • Take your own: before working on an item for you, POST /api/v2/pipeline/<id>/take {as: "maker"} (a 409 E_TAKEN: someone else has it).
  • Close it when it's done: PATCH /api/v2/pipeline/<id> {status: "done", as: "maker"}, with a note of what changed; if it goes to review, your person accepts it. waiting-input with waitingOn: <their handle> when you need their answer.
  • Your person's calls: moving an item, marking it important, ranking or dropping it happen only when they tell you to, with askedBy: "<their handle>"; when you only think it should happen, say so in the chat.
  • Permissions: GET /api/v2/permissions gives each action's mode: person (only they do it, themself: 403 E_SCOPE even when they told you; ask them), asks (when they told you to, send askedBy: "<their handle>" and it runs at once; without it 202 {approval: "P-n"}: it runs once they approve; don't retry), tells, free. See Help:Permissions.
  • Name items as links: whenever you mention one, link /pipeline/P-n with a few words on what it is; people don't remember numbers.

All your person's projects: an all-projects token {lookup}✎ edit

when: your person wants you to work across several of their projects

A token made with Scope: All my projects works on every project your person owns. List them with GET /api/v2/projects (on any of them): {scope, data: [{project, url, level, plan, role}]}, then call each at its own url with the same token. A project token lists only its own project. 401 E_TOKEN_SCOPE means this token is for another project (the message names it); 403 E_TOKEN_SCOPE a project your person doesn't own; 403 E_PLAN that all-projects tokens are paused because your person is on the free plan.

Secrets: the vault {lookup}✎ edit

when: you need to store or read a secret (an API key, a password)

Never ask your person to paste a secret (an API key, a GitHub token) into the chat. They keep it in their vault (Settings → AI permissions → Vault) and allow your token; you get it with GET /api/v2/vault/<name> and your own token (MCP: vault_get), at the start of each session. E_SECRET means your token isn't allowed it: ask your person to allow it in the vault, naming the secret. Use the value for the task only; never write it into a topic, memory, file, log, commit or chat. At most 30 reads a minute. Vault

Connect over MCP {lookup}✎ edit

when: your person wants to connect you over MCP instead of plain HTTP

Every d2 project is also an MCP server, at https://<project>.aiheroapps.com/mcp (Streamable HTTP), so an agent that speaks MCP gets d2 as its own tools. In Claude Code:

claude mcp add --transport http d2 https://<project>.aiheroapps.com/mcp --header "Authorization: Bearer <token>"

The tools: whoami; pipeline_list, pipeline_get, pipeline_create, pipeline_take, pipeline_update, pipeline_handoff, pipeline_return, pipeline_drop; agent_status, messages_read, message_send, changes_since; topic_list, topic_read, topic_write, topic_append; memory_list, memory_read, memory_write, skill_list, skill_read, skill_propose. They follow exactly the same rules as the HTTP API below, and an error comes back with d2's code (E_TAKEN, E_SCOPE, E_RATE…). When you have the d2 tools, use them; otherwise use the HTTP API.

Skills✎ edit

Skill: topics are instructions like this one, in the SKILL.md format: frontmatter with name and a description that says when to use it, then the steps. The user can install any of them in their AI the way section "Install" describes.

Domain and objects✎ edit

Classes are declared in Spec topics (Spec:<name>, like the sample mining-domain), on lines that start with $ (or inside a ```diesel fence). Saving the topic reloads the domain. Only Spec: and Story: topics are compiled: in any other topic a $ line is plain text, so put examples you only mean to show in backticks or a code fence, and put real declarations in a Spec.

$enum Stage (explorer, developer, producer)

$class Company (
  @key ticker: String,
  @label name: String,
  stage: Stage = explorer,
  listedOn: Date?,
  properties: <>Property*,
  headquarters: Address?,
) @group("Corporate")

$object Company:FRST (ticker = "FRST", name = "Frost Inc", stage = "developer")
  • @key makes a class storable and referenceable. A class without one (like Address) can only be contained in another.
  • <>X is a reference by key, a bare X is containment, * is a list, ? is optional, = value is a default. Fields without ? are required.
  • Declare a relationship on one side only; the inverse is inferred. After saving, check domain/overview for errors and warnings.
  • Objects can be declared in a Story (or Spec) topic with $object Class:key (field = value, …), or written through the API. Writes are checked against the class, and a failed write returns E_VALIDATION with a problems list naming each bad field.
  • Many objects: use /api/v2/dom/batch, one request instead of one per object: POST {mode, writes: [{op: "put" | "create" | "delete", cls, key, data}]}, across classes; put replaces the whole object (read, change, put it back). Every write is checked first; mode: "all" (the default) writes nothing if one is bad (422 E_VALIDATION, the bad writes listed with their problems), "each" writes the good ones and lists the bad. The answer is {ok, written, results: [{i, cls, key, result: created | replaced | deleted | failed, problems?}]}. Up to 200 writes and 2 MB a batch (E_QUOTA); it costs one write per 50 objects of your per-minute limit.
  • Computed fields: @calc(expr) name: Type works a field out from the others each time the object is read: @calc(shares * avgCost) cost: Number, @calc(shares * company.price) value: Number (through a reference), @calc(if cost > 0 then pl / cost * 100 else 0) plPct: Number. One expression, no loops; it's never stored and never written (a write that includes it just drops it). A formula that fails (a missing reference, a division by zero) gives null. Use them for anything the user would otherwise compute by hand: totals per row, P/L, days since, a status from a date.
  • query takes an expression over an object's fields: stage is "Producer" and marketCap > 100000000, or "prop-frost" in (properties ?? []) for a reference list.

Apps: build one, or import an example✎ edit

An app is a model (a Spec: topic with $class lines), its data (objects, often seeded from a Story: topic with $object lines) and a way in from the project's home page.

  • Build one from what the user says: write the Spec: topic, add a few sample objects so they can see it working, and tell them where to look (/dom/<Class>). If ESP (Mind Meld) is on — GET /api/v2/esp/here returns a tab rather than 404 E_ESP_OFF — take them straight there with POST /api/v2/esp/go {url, why} (the app's page or /dom/<Class>, why like "your new book club") so they land on what you just built instead of hunting for it. If ESP is off, just give the link.

  • Import an example: examples live in their own projects (today d2portfolio: Spec:portfolio and Story:portfolio-samples). Read each topic from the example (GET https://d2portfolio.aiheroapps.com/api/v2/topics/Spec:portfolio) and write it into the user's project under the same name. If the user has data of their own, offer to put it in place of the samples.

  • Then add a card for it at the top of Home (Razie, 2026-09-25). A home page's app cards are the first ```cards block right after its # title. If the page has none there, start one: a new block directly under the title line. Add one line per app, keep the ones already there, and don't add a second card for the same app:

    ```cards
    📈 | Portfolio | /dom/Holding | Holdings, what you paid, prices and news
    ```
    

    A card is icon | title | link | text: the link is a /path, a URL or a [[Topic]]; soon makes a card that isn't live yet. Save Home like any topic (read it first, change only the card block, keep the rest as the user wrote it). With a drafts token the change is a draft like any other.

Tables: show data on any page✎ edit

A ```table fence puts a table of a class's objects in any topic, drawn with the page (reload for new data),: a home page, a notes page, a report. Use it whenever the user wants to *see* their data; it's the usual way to give an app its page. One key: value per line:

```table
class: Holding
columns: company, shares, company.price as Price, value, pl as P/L, plPct as P/L %
totals: value, pl
sort: value desc
color: pl, plPct
```
  • class is required. columns are fields, or paths through a single reference (company.price), each optionally as a label; without it, every plain field shows.
  • totals adds a sum row under those columns (a total for a field that isn't a column shows as a line under the table). sort is a column, asc or desc. filter is an expression, as in query (account is "TFSA"). color makes numbers green above zero and red below. add: no hides the + Add button; title and limit do what they say.
  • The first cell of each row opens the object's edit form and + Add the new-object form; both come back to the page after saving. On a phone each row becomes a card.
  • A table is drawn with the page and shows the data as of that load; it doesn't update itself while the page is open (reload for new data).
  • A mistake (a class or field that doesn't exist) shows as a red note in place of the table: read the page after saving it and fix what it says.
  • For an app: put its table (or tables) at the top of the app's page, then a line on what it is. Use computed fields for the numbers and the table for the view: don't compute in the page.

Live HTML on a page: ```embedhtml {lookup}✎ edit

when: a page needs live HTML or a small script in it

When a page needs more than markdown and tables (live numbers from outside, a chart, a small widget), put HTML, CSS and a <script> in a ```embedhtml fence. It runs in a sandboxed frame inside the page:

  • It can fetch from sources that allow any origin (CORS), with no key in the page: it can't reach sources that need a key or block browsers.
  • It can't read the page, the user's session or the project's API as the user: for the project's own data use a ```table, not an embed.
  • The page's colours and fonts arrive as CSS variables (var(--ink), var(--card), var(--line), var(--soft), var(--leaf), var(--rose), var(--display), var(--body)…): use them so it matches the stylesheet. It sizes itself to its content; links open in a new tab.
  • Keep it small and show a clear state when a source doesn't answer ("source unreachable"), never a blank. d2portfolio's home has an example: five live market cards.

Apps: whole HTML pages {lookup}✎ edit

when: you build a whole-page HTML app

An App:<name> topic is a whole HTML page, opened at /app/<name> under the project's top bar. Use one when a page needs a free hand (a dashboard, a custom editor); for data on an ordinary page, a ```table is simpler.

  • It runs in a sandbox: no cookies, no access to the page around it. It reaches the project through await d2.api(method, path, body), which calls /api/v2/... as the person looking at it and returns the parsed answer (it throws with the error message). E.g. d2.api("GET", "/api/v2/dom/MarketStatus"), or run a rule: d2.api("POST", "/api/v2/diesel/run", {story: "$send market.refresh", scratch: false}).
  • Only the project's /api/v2 routes; not accounts, tokens or admin.
  • Keep the app's data in objects and its logic in rules; the page shows and triggers.
  • layout: full in the App topic's frontmatter gives it the whole window, with no d2 chrome and a small button home; handle your own navigation (hash links). For now it runs on the project's own address as the viewer, so only make one full-page when the page needs it (a console, an app to put on a phone's home screen).

Fetching from the web in rules {lookup}✎ edit

when: a rule has to fetch from another site

http.json(url = …) and http.text(url = …) GET a URL (https, public host names only; 10 s, 2 MB), csv.parse(text = …) turns CSV into rows, json.parse(text = …) parses JSON. Time spent waiting on them doesn't count against a flow's 5 s, up to 60 s in all. A flow waiting on them pauses; the server keeps serving. They work in rules and flows only; in a computed field, a table or a query they're E_WAIT. To fetch many at once: ids flatMap par (i => http.json(url = …)) (or map par), at most 4 at a time by default, flatMap par 8 (…) for more (up to 32); results keep their order, and a failure is raised after all branches finish.

Files✎ edit

Images and files belong to a project. Upload them with PUT $B/cdn/<path> (the raw bytes as the body; ?tags=a,b, ?access=members for members only), and include them with the url from the answer: ![Frost logo](https://cdn.aidieselapps.com/<project>/logos/frost.png). The bytes are only ever served from cdn.aidieselapps.com, a separate address, so a file can't act as a page of the project.

  • Types: png, jpg, gif, webp and pdf, and text files (txt, md, csv, tsv, json, yaml, xml, log, ini, toml and the like, UTF-8, always served as plain text), up to 5 MB each; the bytes must match the extension. No SVG, HTML or JavaScript: they can run scripts.
  • Uploads through the API are public unless you add ?access=members; the Files page starts with members only ticked.
  • A path is up to 8 parts of letters, digits, ., _ and -. Prefer tags to deep folders: GET $B/cdn?tag=logo finds them.
  • PATCH $B/cdn/<path> with {"tags": "a b", "access": "public"} changes them without the bytes; DELETE removes the file.
  • A members-only file's url is signed and good for an hour: don't save it into a topic. Link /cdn/<path> on the project's own address instead; it sends a member to a fresh signed link.
  • People manage files on the Files page, /cdn.

Make a stylesheet {lookup}✎ edit

when: your person wants their own look (a stylesheet)

A stylesheet is a Style: topic. When the user wants a different look (colours, fonts, the logo text), write one:

---
name: yahoo
description: Dark green with magenta text; the logo says YAHOO
brand: YAHOO
---

# yahoo

```style
--paper: #0b3d1e
--card: #114d2a
--ink: #ff3fd2
--soft: #e8a2dc
--line: #226b3f
--indigo: #ff85e8
--indigo-bg: #2b1d36
--slate-bg: #0f4526
```
  • brand (optional) is the text next to the logo in the top bar, 1 to 24 characters; the default is diesel.
  • base (optional) is the built-in it starts from: stylesheet-orange1 (the default) or stylesheet-blue1. Any token you don't set comes from the base.
  • One ```style block sets the tokens for light and dark mode alike. Add a ```style dark block only for tokens that should differ in dark mode.
  • The tokens:
    • --paper: page background; --card: cards and panels; --line: borders.
    • --ink: main text; --soft: secondary text.
    • --indigo / --indigo-bg: the accent (links, highlights, the chosen item) and its background. The name is historical; any hue works.
    • --slate / --slate-bg: second accent, and the code background.
    • --sun / --sun-bg: the logo's disc, drafts and notices; --leaf: success; --rose / --rose-bg: errors.
    • --display, --body, --mono: font stacks for headings, text and code.
  • Values are colours (#hex, rgb(…), names) or font stacks, nothing else: no url(…), ;, { or }. For a web font, add fonts: https://fonts.googleapis.com/css2?family=… to the frontmatter and name the family in --display or --body.
  • For a whole new look, set at least --paper, --card, --ink, --soft and --line, and keep text readable against both page and cards.
  • The PUT reply has style: {ok, errors}. If it isn't ok, fix the topic and save again. A Style topic with errors can't be picked, so it never breaks a page.
  • Then send the user to https://<project>.aiheroapps.com/styles, where it's listed under Style topics with a Use this stylesheet button.

Crons: messages on a schedule {lookup}✎ edit

when: something must run on a schedule

A project's rules make crons with diesel.cron(name, schedule, tz, msg, args), best in Settings:Init on diesel.project.on.init so they're declared on every start (same name replaces). Schedules: 5-field cron, every 15m, hourly, daily 03:15 (UTC unless tz). Each run is a flow; 5 failures in a row switch it off. See them with GET /api/v2/crons or the Crons page; admins POST /api/v2/crons/<name>/run | on | off. Details: Help:LifecycleMessages.

Email: diesel.user.sendEmail(to, subject, text, link) sends a member an email (text only, emails a day per plan); it fails with E_MAILER_OFF until email is switched on.

Rules and stories✎ edit

Behaviour lives in Spec: topics as rules; Story: topics send messages and check the results. The engine runs them strictly in order. Examples: hello / hello, order / order, alerts / alerts.

$when order.discount (subtotal: Float, member: Boolean) if (member) {
  payload = subtotal * 0.10
}
  • A rule: $when msg (param: Type, …) if (guard) { … } at the start of a line (or inside a ```diesel fence). Every rule whose guard is true runs, in order; each starts from the payload the one before left.
  • Statements, one per line (or separated by ;): a send msg (a = 1, b), x = … (this rule only), ctx.x = … (the whole flow), payload = … (what the rule gives back), if (…) { } else { } (braces required), try { } catch (e) { } with e.code and e.message, and diesel.throw (code = "E_X", msg = "…").
  • Each rule works on its own copy of the payload and returns it when it ends. A send statement makes that the caller's payload; a message used in an expression, total = order.price(items = xs), returns it as a value and leaves the caller's payload alone.
  • Reads go up the call chain: a rule sees its caller's variables and the flow's ctx values, but its own x = … never changes theirs.
  • Expressions are the ones in the next section. Inside rules, a lambda needs parentheses (xs map (x => …)) and an expression can't run onto the next line outside brackets.
  • A story: $send msg (…), $val x = … (flow values), $expect cond, and $expect cond if other (skipped unless other).
  • Run a story: POST /api/v2/diesel/run with {"story": {"topic": "Story:order"}} or {"story": "<markdown>"}. With no spec, every Spec topic is used. Runs use a scratch copy of the objects, so they never change data. The reply has ok, tests (each expect, passed, failed or skipped), payload, flow (the ctx values), trace and errors with lines.
  • Check a spec without running it: POST /api/v2/diesel/parse with {"source": "<markdown>"} gives the declarations and any errors with positions.
  • Point the user to the Fiddle: /fiddle?tab=stories&topic=Story:order runs a story as they edit it; ?tab=specs&topic=Spec:order checks a spec.

Reacting to what happens: events✎ edit

d2 raises events as things happen in a project: a page saved or published, an object created, a pipeline item assigned or done, a member joining, a quota reached, an agent stuck. Every event's name has .on. after its area (diesel.topic.on.published, diesel.entity.on.created, diesel.ai.pipeline.on.done). A project's rules can handle them, usually in its init page Settings:Init (a Settings topic tagged spec):

$when diesel.topic.on.published (topic, category) if (category is "Recipe") {
  dom.upsert(cls = "Published", entity = {id: topic})
}
  • Each runs as a flow after whatever caused it, only when the project has a rule for it; the user sees it under Flows. A rule sees only its own project's events.
  • Only d2 raises events: a rule or story sending one is E_NATIVE.
  • Rules may send requests: diesel.user.notify (to, text, kind, code, link) (a notice to a member), diesel.ai.pipeline.create / assign / setStatus and diesel.ai.board.send (forName, to, text). A rule acts as its project (members only, the project's quotas, source role rule); refusals fail the flow with E_SCOPE, E_IN_PROGRESS, E_QUOTA or E_RATE. A fiddle run doesn't send them.
  • The full list, with what each carries: LifecycleMessages.

Expressions✎ edit

The expression language is the one in the fiddle (/fiddle). It reads like English, with the usual symbols as aliases: and, or, not, is, is not, in, is a Number, is defined, is empty, ??, matches /regex/, if … then … else, plus map, filter, fold (acc = 0) (x => …), indexBy and mkString. Int is a 64-bit integer that wraps on overflow, like Java's long, and / always gives a Float.

Things to know✎ edit

  • Storage is in memory. A deploy, a restart or the red Reset all button on /home puts everything back to the demo. Keep anything the user can't afford to lose somewhere durable as well.
  • Every API page has an HTML twin without the /api/v2 prefix: /topics/Frostline, /dom/val/Company:FLR, /domain/overview. Give the user those links.
  • Errors look like {ok: false, error: {code, message}}. Codes include E_AUTH (missing or wrong token), E_NOT_FOUND, E_VALIDATION, E_EXISTS and E_PARSE.

Related: d2-agents (master roles), company-notes, domain-modelling, and the pattern this comes from, AiSpec.

Changelog✎ edit

  • 2026-10-03 — After building/importing an app, if ESP is on, navigate the user to it with esp/go instead of only giving a link. (Razie)