---
name: sketch
description: Sketch an App workflow as reviewable JSON before writing code, and render it as a phone-friendly flow diagram.
user-invocable: true
---

# Sketch

Use this skill to agree on an App's architecture with a human before building
it. A sketch is a small JSON file that `sketch.html` renders as a vertical flow
diagram. Write it from the user's description alone. Don't read SDK code or
look up API details to sketch; that happens when you build.

The skill directory holds `sketch.html` (viewer), `link.mjs` (link and check
tool) and `examples/`. In this repo it is `skills/sketch/`; in an App it is
`node_modules/@notionhq/apps/skills/sketch/`.

## Iterate on a sketch

1. Write `sketches/<name>.sketch.json` unless the user names another path.
   Start from the closest file in `examples/`.
2. Run `node <skill dir>/link.mjs sketches/<name>.sketch.json --open`. Fix
   every error it prints and consider each warning. The command prints a link
   to the hosted viewer and opens it in the browser. Give the user the link
   too.
3. The user reviews in the browser and can edit the JSON there. The link
   updates as they edit, and **Copy link** and **Copy JSON** hand it back.
4. When the user pastes a link, run `node <skill dir>/link.mjs decode '<link>'`,
   save the result over the sketch file, and summarize what changed.
5. Once the user agrees, build it. Use the sketch as the spec and read the
   matching skills, such as [Workflows](../workflow/SKILL.md).

`node <skill dir>/link.mjs check <file>` checks without printing a link. The
hosted viewer at `https://makenotion.github.io/apps-sdk/sketch.html` is
published from `main`, and the sketch travels in the link's fragment, so the
site never sees it. Pass `--local` to link to a viewer on
`http://127.0.0.1:4321` served from this copy of `sketch.html` instead, such as
when changing the viewer or working offline. It starts if it isn't running and
stops after an hour without requests. Use `--port` if 4321 is taken, or
`--base <url>` to link to another copy of the viewer.

## Format

```json
{
	"version": 0,
	"name": "Space IDs → Statsig",
	"resources": [
		{
			"id": "spaces",
			"label": "Spaces",
			"icon": "🏢",
			"properties": { "Space ID": "text" }
		}
	],
	"connections": [{ "id": "statsig", "label": "Statsig", "notes": "Console API key." }],
	"state": { "pageId": "…", "spaceId": "abc123" },
	"nodes": [
		{
			"id": "segment",
			"label": "Add space ID to Statsig segment",
			"reads": ["spaceId"],
			"uses": ["statsig"],
			"out": null
		}
	],
	"edges": [
		{
			"from": null,
			"to": "segment",
			"label": "When a row is added to Spaces",
			"uses": ["spaces"],
			"shape": { "pageId": "…", "spaceId": "abc123" },
			"writes": ["pageId", "spaceId"]
		},
		{ "from": "segment", "to": null }
	]
}
```

- **Workflows.** An App with more than one workflow lists them in `workflows`,
  each with an `id`, a `name`, and its own `state`, `nodes` and `edges`.
  Resources and connections are shared. The top-level form above is shorthand for a single
  workflow. Workflows hand off through resources: one writes a database, and
  another's trigger watches it. Trigger on created pages or gate on a property
  so a workflow doesn't retrigger itself. See `examples/calendar-research.sketch.json`.
- **Resources** are things the App declares in Notion as Code, such as
  databases. Give a database its `properties` and an emoji `icon`. Use the
  same emoji for the same kind of database in every sketch: 📅 for events,
  👤 for people. Connections can have an `icon` too; without one they show a
  plug.
- **Connections** are external accounts someone sets up for the App, such as
  Google Calendar, Slack or an API key. Give each an `id`, a `label` and
  optional `notes`. Nodes and triggers refer to resources and connections with
  `uses`, so their ids must be unique across both.
- **State** is a shared key-value store. Its values are examples that show the
  shape. Nodes list the keys they `reads` and `writes`. Suffix a read with `?`
  when the key is legitimately unset on some paths, such as a loop's first run.
- **Nodes** are generic steps. `label` says what happens in plain language:
  "Wait 20 seconds" or "Review agent reviews the PR". Optional `notes` hold
  details worth keeping.
- **Agentic steps.** Set `agentic: true` on a node when an agent does the
  work, such as judging, researching or writing, rather than code that does the
  same thing every run. The viewer outlines it in purple so a reviewer can see
  where judgment comes in. Leave it out for deterministic steps.
- **Ports.** Every node has `out`. It may also declare `aux` (`label`, optional
  `color` of amber, green or blue, and `shape`) and `error`
  (whose `shape` describes `data`). Each run of a node sends on exactly one of
  out, aux or error. Red is reserved for error paths, so pick another color
  even when aux is a failure, like "tests fail". Purple marks agentic steps,
  so aux doesn't use it either. `out: null` means the node
  hands nothing on, like a 204.
  An error always carries `{ code, message, data? }`. Declare `error` only
  when you wire it. Any step can fail without one, and that fails the workflow.
- **`out` is the golden path.** The diagram follows `out` edges from each
  trigger straight down the middle, so a reviewer can read the expected run
  first. Nodes reached only from `aux` sit in a lane on the right, and nodes
  reached from `error` sit on the left. If the expected result of a step is
  what you'd call aux, such as "approved", make it `out` and the other result
  aux.
- **Edges.** `from` is `"node"` (out), `"node.aux"` or `"node.error"`. `to` is
  a node id. A node has up to 4 incoming edges, and each output port has at
  most one edge. `from: null` is a trigger, which carries `label`, `shape`, and
  optionally `writes` and `uses`. `to: null` ends the workflow. Every out port
  must be wired to a node or to null. Wire an error to null to end quietly.
- **Loops** need a `limit` on the edge that loops back: the maximum number of
  times that edge is taken.

Put data on a wire when only the next node uses it. Put it in state when a node
further along or a later loop iteration reads it.

## From sketch to code

| Sketch            | Workflow code                                                  |
| ----------------- | -------------------------------------------------------------- |
| Trigger edge      | A typed trigger; its `writes` come from the event              |
| Node              | `context.step(label, …)`, or the API the label describes       |
| `agentic: true`   | A custom agent the step calls, declared in Notion as Code      |
| State key         | A handler variable assigned from event data or a step result   |
| Wire payload      | The step's return value                                        |
| `aux`             | A branch on the step's result                                  |
| Edge with `limit` | A bounded loop whose steps use a composite `key` per iteration |
| Wired `error`     | Catch the step's error and continue on that path               |
| Edge to `null`    | The handler's return                                           |
| Resource          | A Notion as Code declaration                                   |
| Connection        | A connection the App needs, set up outside the code            |

Keep node ids in step names or keys so the code traces back to the sketch.
