int-ai

Documentation

2 · The Vault — the space as files

This page is the canonical reference for everything on disk. If any other page disagrees with this one, this one wins. Format v2 (portals layout), current as of 2026-08.

The product's one structural idea: the entire workspace — every canvas, note, drawing, image, setting — is a folder of plain files on the user's own devices, called the vault. The files are the data, not a copy of it. The app reads the same files you do. That is why you are a first-class participant: any agent that can read and write this folder is already integrated. There is no API, no SDK, no server to call.

Platform, plainly: today Int.ai is an iPhone/iPad app. The computer side is just the synced folder — no desktop app. iCloud carries the sync by default (4 · Integration covers alternatives).


Hard rules

Read these before writing anything. None is arbitrary: the vault has no server validating writes — these rules are the write contract, and each carries its mechanical reason so you can reason from it instead of memorizing it.

  • ALWAYS include status on any node you write"processed" for your own. status is the capture-lifecycle field the whole inbox pattern runs on (schema below), so the app treats a node without one as incomplete and silently drops it — never rendered, no error. The number-one write failure.
  • ALWAYS mark your writes "source": "agent". Two protections hang on this field: the watcher skips agent-sourced nodes, so your replies can never re-fire a trigger and cascade (3 · The Loop) — and sweeps treat non-agent captures as the user's unprocessed thinking, so a mislabeled write becomes a "capture" your own system keeps re-handling. Set color explicitly with it: one consistent agent color is how the user knows your work on sight.
  • ALWAYS name a new node/edge file by its id — an UPPERCASE UUID (8A21..., never 8a21...), filename <ID>.json. Uppercase is the app's own convention, and one casing everywhere keeps any id findable byte-for-byte with plain tools; filename-equals-id is what makes a fresh write collision-proof and addressable forever.
  • NEVER read identity off a filename. Filenames are human labels the app rewrites freely (a node file becomes its first line); identity lives inside the file (canvas_id, id, note_id) and never moves. Resolve by the id inside and a rename can never break you.
  • NEVER reference media by URL. The vault is self-contained on purpose — it must render on the phone with nothing fetched, and stay whole when copied, moved, or backed up. Media lives in assets/ and is referenced by relative path.
  • NEVER delete or rewrite the user's files (nodes with source: "user", their notes, their assets) unless the user's configuration explicitly says to. Your edits land directly in the source of truth — there is no undo gate protecting their thinking from you.
  • After any write, verify: re-read the file, confirm it parses, confirm the required fields match the schemas below. The app repairs structure on read; it does not backfill required fields — so an unverified bad write looks fine on your side and simply never appears.

Vocabulary

Six words, used identically across this whole guide:

  • vault — the folder; the whole workspace.
  • portal — one spatial canvas (the app's word for a canvas; canvases/ is only the legacy folder name). One vault has many portals.
  • node — one card on a portal: text, media, or drawing. One node = one file.
  • note — one markdown document. The linear surface beside the spatial one.
  • tag — a literal #word typed inside a node's or note's text. That's the entire storage mechanism — there is no tags field. Meaning comes from configuration (3 · The Loop).
  • edge — one connection between two nodes.

Layout

IntAiVault/
├── portals/<Name>/              # one portal; folder name = its display name
│   ├── canvas.json              # manifest — canvas_id inside is the identity
│   ├── nodes/<Label>.json       # ONE node per file
│   └── edges/<EDGE-ID>.json     # one connection per file
├── notes/<Title>.md             # frontmatter + markdown; note_id inside is the identity
├── assets/<hash>.<ext>          # media files, referenced by relative path
│   └── drawings/<NODE-ID>.png   # rendered images of drawings (for your vision)
├── config/
│   └── triggers.json            # the tag/trigger list — see 3 · The Loop
└── .intai/                      # app metadata and load/migration logs — read-only for you

Finding the vault (requires: file access on the user's computer): it lives under the app's iCloud container — the same container id for every install:

~/Library/Mobile Documents/iCloud~com~jonah~Int-Ai/Documents/IntAiVault

If it isn't there, fall back to signature search — the folder containing both .intai/ and config/triggers.json. If it exists nowhere, iCloud hasn't mirrored the phone yet: confirm the same iCloud account on both devices, open the app once on the phone, and give the first sync a few minutes. Resolve the absolute path once and record it in your system (4 · Integration).

One rule reconciles every filename question: reading, treat names as labels and resolve by the id inside; writing, name the file by its id. The app derives friendly names from content on its own edits (a node file becomes its first line, a note file becomes its title) and never renames a UUID-named file behind you — your <ID>.json stays valid forever. A vault written before mid-2026 may still have canvases/<UUID>/ folders beside or instead of portals/ — read both; never create new content under canvases/.

Node schemas

Text node — the shape you'll write most

Copy this shape exactly (a real, valid file — portals/Ideas/nodes/2F6A0C4E-8B1D-4E2A-9C3F-D5A7B4E19A02.json):

{
  "id": "2F6A0C4E-8B1D-4E2A-9C3F-D5A7B4E19A02",
  "type": "text",
  "position": { "x": 640, "y": 320 },
  "width": 240,
  "height": 100,
  "content": "**Pricing thought** — the tier names read as sizes, not outcomes.",
  "color": "#3B82F6",
  "source": "agent",
  "status": "processed",
  "timestamp": "2026-08-03T14:22:05Z"
}
  • id — uppercase UUID; must equal the filename.
  • type"text" here; other values below.
  • position — the node's center, in canvas units. To reply beside a node, use its x + 250, same y. On an empty portal, anywhere near the origin works ({ "x": 300, "y": 300 } is a fine first node); the app's viewport finds content.
  • width / height / timestamp — optional; sensible defaults fill in.
  • content — markdown (the text model below). Tags live in here as literal #words.
  • color — explicit hex. Meaning is yours to assign; be consistent.
  • source"agent" for every write of yours; the app writes "user".
  • status — the capture-lifecycle field, and the one that bites:
    • "unprocessed" — a capture the user's system hasn't handled yet. User captures are born unprocessed — this is the field that makes the vault an inbox you can sweep.
    • "processed" — handled. Your own writes ship as this.
    • "on_hold" — parked (set by the holdback trigger action). Still renders; skipped by sweeps.
    • Absent — the node never renders. This is the number-one write failure.
✗ Wrong: { "id": "2f6a0c4e-...", "body": "text", "tags": ["#idea"] }
✓ Right: { "id": "2F6A0C4E-...", "content": "text with #idea inline", "status": "processed", ... }

(lowercase id, invented body/tags fields, missing status — the three classic misreads)

Media node (photo · scan · voice · file)

Same shape as a text node, with type of "photo", "scan", "voice", or "file", plus an asset field holding the relative path to the media file:

{
  "id": "7C09E1B3-4D2F-4A86-B15E-0F3C6A92D7E4",
  "type": "photo",
  "position": { "x": 900, "y": 320 },
  "width": 260, "height": 200,
  "content": "whiteboard after the pricing discussion",
  "asset": "assets/9f2c81d4a07b.jpg",
  "color": "#64748B",
  "source": "agent", "status": "processed",
  "timestamp": "2026-08-03T14:25:11Z"
}

content doubles as caption and alt-text — it's also where a tag on a media capture lives. Voice captures are transcribed on-device; the transcript lands in content, so you read a voice node like a text node.

Drawing node

A drawing is a node with type: "drawing", a strokes array (the truth), and a render path — a PNG the app maintains so you can see the drawing:

{
  "id": "5B8D2A17-9E4C-4F03-A6B1-C2D7E8F3A951",
  "type": "drawing",
  "position": { "x": 480, "y": 620 },
  "width": 240, "height": 180,
  "content": "napkin sketch of the onboarding flow",
  "strokes": [ { "points": [[-12, -4], [0, 3], [14, 9]], "color": "auto", "width": 3, "tool": "pen" } ],
  "render": "assets/drawings/5B8D2A17-9E4C-4F03-A6B1-C2D7E8F3A951.png",
  "source": "user", "status": "unprocessed",
  "timestamp": "2026-08-03T09:12:44Z"
}

To read a drawing, open its render PNG with vision. You'll rarely write one; if you do, points are offsets from position (the center), "color": "auto" adapts to the theme, and the app repairs envelope mismatches on read.

Edge schema

One connection per file — portals/<Name>/edges/<EDGE-ID>.json, UUID filename (edges keep UUID names even in the app; connections have no human name worth deriving):

{
  "id": "E4A7C2D9-1B6F-4E58-A3C0-8D5B9F2E7A14",
  "from_node_id": "2F6A0C4E-8B1D-4E2A-9C3F-D5A7B4E19A02",
  "to_node_id": "7C09E1B3-4D2F-4A86-B15E-0F3C6A92D7E4",
  "label": "",
  "source": "agent",
  "timestamp": "2026-08-03T14:26:00Z"
}

Required: id, from_node_id, to_node_id. Optional: label (empty string is fine), source, timestamp. Edges carry no status — that field is a node concept. version (here and in the manifest) is the app's merge bookkeeping: write 1 on a fresh file, never bump it yourself.

Node ids are uppercase in iOS-written files but compare ids case-insensitively when matching — an edge whose casing disagrees with its node still renders, and yours should tolerate the same. An edge pointing at a missing node is dropped on read (harmless, invisible).

Portal manifest — canvas.json

{
  "canvas_id": "A1B2C3D4-E5F6-4708-9A0B-C1D2E3F4A5B6",
  "name": "Ideas",
  "hidden": false,
  "viewport": { "offsetX": 0, "offsetY": 0, "scale": 1.0 },
  "updated_at": "2026-08-03T14:00:00Z",
  "version": 1
}

To create a portal: make portals/<Name>/ with this manifest (fresh uppercase canvas_id, name matching the folder) plus empty nodes/ and edges/ folders. viewport is where the user's camera last sat — write the zeros above and the app takes over. To find a portal, match the canvas_id inside each manifest — never trust the folder name.

Note schema — notes/<Title>.md

A note is a markdown file: YAML frontmatter + body. The filename is the sanitized title and the app renames the file when the title changes — so find an existing note by grepping frontmatter for its note_id, never by guessing the filename.

---
note_id: 3D9F5B21-7A4E-4C68-B0D2-E8A1C6F4B7D3
title: Pricing research
category: Work
hidden: false
created_at: 2026-08-03T14:30:00Z
updated_at: 2026-08-03T14:30:00Z
version: 1
---

# Pricing research

Three findings worth your attention:

- [ ] the tier names test badly — see the ==highlighted== quote
- competitor grid: ![grid](../assets/4e1a7c22b9d0.png)

The first body line is the title — keep filename, frontmatter title, and first line in agreement on a fresh write; to retitle, edit the first line and let the app move the file (an external rename gets renamed back on next save).

The text model — everywhere text lives (note bodies and node content alike): GitHub-Flavored Markdown (headings, lists, checklists, tables, links, code, images) plus ==highlight==. Rich extras are a fixed inline-HTML set — <span style="color:#RRGGBB"> and <u> — and nothing else survives read; other HTML is stripped to plain text.

Assets

Media files live in assets/, referenced by relative path (assets/<name>.<ext> from a node's asset field, ../assets/<name>.<ext> from inside a note body). The app names by content hash for dedup; any filename you choose works — the reference is the path, not the name. The one exception: assets/drawings/<NODE-ID>.png is keyed by node id on purpose (a stroke edit overwrites in place) — treat that subfolder as the app's.

How the vault behaves

  • Per-file is the concurrency model. You edit one node while the user edits another — different files, no clash. The one shared file is config/triggers.json (3 · The Loop): read-modify-write it quickly and re-read after writing; the app writes the same file when the user edits triggers, and last write wins.
  • Repaired on read, within limits. Malformed JSON is repaired, out-of-range values clamp, bad enums coerce, dangling edges drop, disallowed markup strips. Repair is structural only — it will not backfill status, and it will not fix bad judgment (a reply on the wrong portal).
  • Sync is the transport, files are the truth. iCloud by default; the app picks up external changes (yours) and re-renders — a write you make on the computer appears on the phone when sync carries it, typically seconds to a couple of minutes.
  • Conflicts resolve last-write-wins per file. Per-file granularity makes real conflicts rare.
  • Verifying a write (requires: nothing but the file): re-read it — parses? required fields present? id uppercase and matching the filename? If yes, the write is good; the remaining latency is sync, not correctness. The definitive check is the app rendering it — during setup, use the user's own eyes once (onboarding builds this in), then trust the re-read.

That's the whole space. Next: 3 · The Loop — how a capture in this folder reaches you, and how a conversation lives here.