---
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

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

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

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 <project>.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 <project>.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

**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).

```bash
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

- **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}

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: [[d2.Help: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)

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 <project>. 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

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

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

- 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

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}
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)

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

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}
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}
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. [[Help:Vault]]

## Connect over MCP {lookup}
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

`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

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.

```d2
$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

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

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}
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}
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}
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

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}
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:

~~~markdown
---
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}
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

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

```d2
$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

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`):

```d2
$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: [[Help:LifecycleMessages]].

## Expressions

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

- **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: [[Skill:d2-agents]] (master roles), [[Skill:company-notes]], [[Skill:domain-modelling]], and the pattern this comes from, [[metals.Topic:AiSpec|AiSpec]].

## Changelog

- 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)
