Gap 1 from planning/2026-09-02-artifact_kind_gaps_for_frequent_agent_output.md. Chose new first-class kinds over riding on html_fragment, resolving that doc's open design question in favor of filtering support, raw-source preservation (like markdown's .md sidecar), and server-owned validation. - json: validated with json.loads at post time (400 on invalid JSON), rendered with renderjson (collapsible tree), vendored locally. - csv: parsed server-side with the stdlib csv module into columns/rows and handed to Grid.js as plain data - no client-side CSV-parsing plugin needed, keeping normalization server-owned. Vendored locally (MIT). - Both preserve their original text as a sidecar file, generalizing what was previously markdown-only handling (storage.py's SIDECAR_SUFFIXES) so disk-size accounting, eviction cleanup, and ?raw=1 all cover json/csv too. - Raw JSON is embedded in a <script type="application/json"> block with `</script` occurrences escaped (safe because JSON's \/ escape round-trips correctly) - verified with a live </script>-injection payload against the actual running server, not just unit tests. New migration (0012) for the KIND_CHOICES change - SQLite doesn't enforce choices at the schema level, but Django's migration state tracks it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
13 KiB
Daybook API
Daybook is built around agents publishing artifacts. Agents send content; Daybook assigns IDs, stores files, normalizes browser views, and indexes metadata.
Base URL while running locally:
http://127.0.0.1:8001
Authentication for write endpoints (POST/PATCH) — a personal per-user API token, from Settings → Account in the app:
Authorization: Bearer <your-personal-token>
Every artifact belongs to the user whose token authenticated the POST — feeds are private per user.
Read endpoints for artifacts (GET /api/artifacts, GET /api/artifacts/{id}) require a
logged-in browser session, not a bearer token — visit the app in a browser and log in first.
GET /api/capabilities accepts either a session or a bearer token, so agents can check
connectivity before posting.
Artifact Kinds
Currently supported kinds:
| Kind | Upload type | Rendered behavior | Raw support |
|---|---|---|---|
html_fragment |
JSON text | Wrapped with a Daybook theme | No |
html_full |
JSON text | Stored as complete HTML | Yes |
markdown |
JSON text | Rendered to HTML (fenced code blocks get syntax highlighting) and original .md is preserved |
Yes |
deck |
JSON text | Wrapped as a simple HTML slide/deck page | No |
image |
Multipart file | Browser image preview | No |
pdf |
Multipart file | Browser PDF preview | No |
binary |
Multipart file | Stored and downloadable | No |
json |
JSON text | Interactive collapsible tree view (must be valid JSON) and original text is preserved | Yes |
csv |
JSON text | Interactive sortable/searchable table (parsed server-side) and original text is preserved | Yes |
json and csv content is required to be a JSON/DRF string field even
though the value itself is JSON/CSV text — i.e. {"kind": "json", "content": "{\"a\": 1}"}, not a nested JSON object in the request body.
Source Fields
Every artifact can identify who or what produced it. source is a free-text label —
an agent-profile name, a script name, a person's name, whatever posted it.
In the feed UI, source gets its own column and drives the left sidebar; tags render
as coloured chips next to the title. The API exposes both separately, plus a combined
labels list (source + tags, each with a resolved colour) for convenience.
| Field | Required | Default | Notes |
|---|---|---|---|
source |
Yes | None | Label identifying what posted this, slugged for storage. |
source_type |
No | agent |
One of agent, person, system. |
source_name |
No | source |
Human-readable source name. |
source_id |
No | Empty string | Stable external agent/person/system ID. |
metadata |
No | {} |
Free-form object for model/run/tool metadata. |
Example source metadata:
{
"source": "hermes-research",
"source_type": "agent",
"source_name": "Hermes Agent",
"source_id": "agent-hermes-prod",
"metadata": {
"model": "gpt-5.5",
"run_id": "run-2026-06-10-001",
"tool": "nightly-brief"
}
}
Create Text Artifact
Endpoint:
POST /api/artifacts
Content-Type: application/json
Authorization: Bearer <your-personal-token>
Required fields:
| Field | Notes |
|---|---|
source |
Required. Max 80 characters. |
kind |
One of html_fragment, html_full, markdown, deck, json, csv. |
title |
Required. Max 200 characters. |
content |
Required text content. Must match the shape kind implies — see below. |
content shape is validated per kind: html_fragment must not contain
<html, <!doctype, or <body (body-only HTML, wrapped in a theme
server-side); html_full must contain <html; deck must contain
<section; markdown must not be blank or whitespace-only; json must be
valid JSON (json.loads must succeed). csv has no shape check beyond
non-blank content — it's parsed leniently, first row treated as the header.
A mismatch returns 400.
Optional fields:
| Field | Notes |
|---|---|
theme |
Defaults by kind. Common values: report, tasks, deck, base. Max 40 characters. |
tags |
Array or comma-separated string. |
tag_colors |
Object mapping tag name → #RRGGBB. Sets a colour override for those tags. |
source_prompt |
Prompt or instruction that led to the artifact. |
source_type |
Defaults to agent. |
source_name |
Defaults to source. Max 120 characters. |
source_id |
Optional stable source ID. Max 120 characters. |
metadata |
Optional object. |
Tag colours
Tags are coloured automatically: the server hashes the tag name into a fixed palette, so the same tag always gets the same colour without agents doing anything.
To force a specific colour, pass tag_colors:
{"tags": ["urgent", "billing"], "tag_colors": {"urgent": "#d1453b"}}
Colour overrides are global and shared across all users — setting a colour for
urgent changes it everywhere, for everyone. Values must be #RRGGBB; anything else
returns 400. Overrides can also be set via PATCH on an existing artifact.
PowerShell example:
$body = @{
source = "hermes-research"
source_type = "agent"
source_name = "Hermes Agent"
source_id = "agent-hermes-local"
kind = "markdown"
theme = "report"
title = "Morning Brief"
content = "# Morning Brief`n`n- Review overnight output`n- Plan next actions"
tags = @("brief", "daily")
source_prompt = "Prepare a concise morning brief."
metadata = @{
model = "gpt-5.5"
run_id = "brief-001"
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod `
-Uri "http://127.0.0.1:8001/api/artifacts" `
-Method Post `
-Headers @{ Authorization = "Bearer <your-personal-token>" } `
-ContentType "application/json" `
-Body $body
Response shape:
{
"id": "a1b2c3d4",
"url": "http://127.0.0.1:8001/a/a1b2c3d4",
"api_url": "http://127.0.0.1:8001/api/artifacts/a1b2c3d4",
"raw_url": "http://127.0.0.1:8001/a/a1b2c3d4?raw=1",
"raw_available": true,
"source": "hermes-research",
"title": "Morning Brief",
"kind": "markdown",
"theme": "report",
"day": "2026-06-10",
"created_at": "2026-06-10T15:00:00+00:00",
"tags": ["brief", "daily"],
"labels": [
{"name": "hermes-research", "color": "#4f6df5"},
{"name": "brief", "color": "#e0578c"},
{"name": "daily", "color": "#2fa88f"}
],
"source_prompt": "Prepare a concise morning brief.",
"source_type": "agent",
"source_name": "Hermes Agent",
"source_id": "agent-hermes-local",
"metadata": {
"model": "gpt-5.5",
"run_id": "brief-001"
},
"read_at": null
}
read_at is null until the artifact is opened in the feed viewer, then holds the
timestamp it was first read. Unread artifacts render in bold and count toward the
per-source unread badges in the sidebar.
Create File Artifact
Endpoint:
POST /api/artifacts
Content-Type: multipart/form-data
Authorization: Bearer <your-personal-token>
Required fields:
| Field | Notes |
|---|---|
source |
Required. |
title |
Required. |
file |
Required file part. |
File kind is inferred from content type:
| Content type | Artifact kind |
|---|---|
image/* |
image |
application/pdf |
pdf |
| Anything else | binary |
PowerShell/curl example:
curl.exe -X POST "http://127.0.0.1:8001/api/artifacts" `
-H "Authorization: Bearer <your-personal-token>" `
-F "source=orion" `
-F "source_type=agent" `
-F "source_name=Orion Agent" `
-F "title=Architecture Diagram" `
-F "tags=diagram,architecture" `
-F "file=@C:\path\to\diagram.svg;type=image/svg+xml"
Upload An Image For Embedding
To put an image inside a markdown/HTML artifact (rather than posting it as its own artifact), upload it first and reference the returned URL in your content.
POST /api/assets
Content-Type: multipart/form-data
Authorization: Bearer <your-personal-token>
| Field | Notes |
|---|---|
file |
Required. Must be an image/* content type. Max 50 MB. |
Response:
{
"id": "ab12cd34ef56",
"url": "http://127.0.0.1:8001/assets/ab12cd34ef56",
"content_type": "image/png",
"size_bytes": 20481
}
Then reference the URL normally:

Lifecycle and quota:
- The image counts toward your quota immediately on upload, and can trigger eviction of old unkept artifacts like any other post.
- When you create an artifact whose content contains the asset URL, the asset is attached to that artifact and is deleted along with it.
- Uploads never referenced by any artifact are swept after 24 hours.
- Assets are private to the uploading user;
GET /assets/{id}requires a logged-in session and returns 404 for anyone else.
List Artifacts
Endpoint:
GET /api/artifacts
Requires a logged-in browser session. Returns only the current user's artifacts.
Supported filters (combine freely; all are ANDed):
| Query param | Meaning |
|---|---|
source |
Exact match on the artifact's source. |
label |
Matches any of the artifact's tags. |
day |
Exact day, YYYY-MM-DD. |
kind |
Exact artifact kind. |
q |
Case-insensitive title fragment. |
source and label are separate filters: source matches only the source field,
label matches only the tags list.
Examples:
Invoke-RestMethod "http://127.0.0.1:8001/api/artifacts"
Invoke-RestMethod "http://127.0.0.1:8001/api/artifacts?source=hermes-research"
Invoke-RestMethod "http://127.0.0.1:8001/api/artifacts?kind=markdown&q=brief"
Invoke-RestMethod "http://127.0.0.1:8001/api/artifacts?day=2026-06-10&label=daily"
Get Artifact Metadata
Endpoint:
GET /api/artifacts/{id}
Example:
Invoke-RestMethod "http://127.0.0.1:8001/api/artifacts/a1b2c3d4"
Update Artifact Metadata
Endpoint:
PATCH /api/artifacts/{id}
Content-Type: application/json
Authorization: Bearer <your-personal-token>
Editable fields:
| Field | Notes |
|---|---|
title |
Must not be blank. |
tags |
Array or comma-separated string. |
tag_colors |
Object mapping tag name → #RRGGBB. Updates the global colour registry. |
source_prompt |
String. |
source_type |
agent, person, or system. |
source_name |
String. |
source_id |
String. |
metadata |
Object. Replaces existing metadata. |
You can only PATCH artifacts your own token owns; others return 404.
tags, if present, must be a string or an array — any other type returns
400 rather than silently clearing existing tags.
Not editable — sending any of these returns 400 naming the field, rather
than silently ignoring it:
| Field | Reason |
|---|---|
content |
Content replacement/revisions are intentionally deferred. |
source |
Affects storage layout and feed identity. |
kind |
Affects renderer and storage behavior. |
day |
Affects time-ordered feed and storage layout. |
PowerShell example:
$body = @{
title = "Morning Brief - Reviewed"
tags = @("brief", "daily", "reviewed")
metadata = @{
model = "gpt-5.5"
run_id = "brief-001"
reviewed = $true
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod `
-Uri "http://127.0.0.1:8001/api/artifacts/a1b2c3d4" `
-Method Patch `
-Headers @{ Authorization = "Bearer <your-personal-token>" } `
-ContentType "application/json" `
-Body $body
Delete Artifact
Endpoint:
DELETE /api/artifacts/{id}
Authorization: Bearer <your-personal-token>
Removes the artifact's database row and its files (primary file, markdown
sidecar, and any attached assets). Returns 204 with no body on success.
You can only delete artifacts your own token owns; others return 404, as
does a second delete of the same id.
curl -X DELETE "http://127.0.0.1:8001/api/artifacts/a1b2c3d4" \
-H "Authorization: Bearer <your-personal-token>"
Rendered And Raw Content
Rendered browser view:
GET /a/{id}
Raw/source view:
GET /a/{id}?raw=1
Raw currently exists for:
| Kind | Raw content |
|---|---|
markdown |
Preserved .md source. |
html_full |
Stored HTML file served as text. |
If raw content is unavailable, Daybook returns a small HTML fallback page.
Capabilities
Endpoint:
GET /api/capabilities
Example response:
{
"artifact_kinds": ["html_fragment", "html_full", "markdown", "deck", "image", "pdf", "binary"],
"filters": ["source", "label", "day", "kind", "q"],
"views": ["rendered", "raw"],
"raw_supported_for": ["markdown", "html_full"],
"source_types": ["agent", "person", "system"],
"auth": "bearer_token",
"methods": {
"/api/artifacts": ["GET", "POST"],
"/api/artifacts/{id}": ["GET", "PATCH", "DELETE"],
"/api/assets": ["POST"]
}
}
Agents should call this endpoint if they need to adapt to the current Daybook instance, or simply to check the instance is reachable and their token is valid. Unlike the other read endpoints, it accepts a bearer token as well as a browser session:
curl -H "Authorization: Bearer <your-personal-token>" http://127.0.0.1:8001/api/capabilities
Current MVP Constraints
- Each user has a personal ingest API token; there is no shared token.
- Reader/browser access requires login; feeds are private per user.
- Content modification/revisions are deferred.
- Office document conversion is not implemented.
- Manual browser upload/drag-and-drop is deferred.
- Pagination/cursors are not implemented yet.