Install
openclaw skills install write-opennoteWrite, update, and read OpenNote notes through the public API. Use when the user wants to publish notes, upload note images, manage labels, or look up previously written notes.
openclaw skills install write-opennoteWrite a note to OpenNote using the public API at https://api.opennote.cc/api/v1.
diaries:write — create and update notesimages:write — upload images to notesmilo_pat_v1_ and is only shown onceSet OPENNOTE_API_TOKEN in your OpenClaw environment:
Option A — OpenClaw config (recommended):
In your OpenClaw settings, add the environment variable:
OPENNOTE_API_TOKEN=milo_pat_v1_your_token_here
Option B — Shell profile (if running locally):
export OPENNOTE_API_TOKEN="milo_pat_v1_your_token_here"
Add this to your ~/.zshrc or ~/.bashrc to persist it across sessions.
Security:
OPENNOTE_API_TOKEN401 INVALID_API_TOKEN error, your token has expired or been revokedThe token is read from the OPENNOTE_API_TOKEN environment variable. It must have the diaries:write scope. If images will be uploaded, images:write is also required.
This skill maintains two local files in the working directory under .opennote/:
.opennote/opennote-labels-cache.json — cached user labels.opennote/opennote-history.json — log of all notes written/edited via this skillAlways read these files at the start of every invocation. If they are missing, treat them as empty. If the user asks about previously written notes, consult the history file.
Read .opennote/opennote-labels-cache.json. If it exists and was fetched less than 24 hours ago, use the cached labels.
Expected cache format:
{
"fetchedAt": 1741111111111,
"labels": [
{ "categoryID": 1, "categoryName": "Personal", "categoryColor": 4294198070 },
{ "categoryID": 2, "categoryName": "Work", "categoryColor": 4283215696 }
]
}
If the cache is missing, empty, or stale (older than 24 hours), fetch labels from the API:
curl -s "https://api.opennote.cc/api/v1/labels" \
-H "Authorization: Bearer $OPENNOTE_API_TOKEN"
Response:
{
"labels": [
{ "categoryID": 1, "categoryName": "Personal", "categoryColor": 4294198070, "visibility": true, "coverImageName": null, "lastModified": 1741111111111, "fontFamily": null, "backgroundPreset": null }
]
}
After fetching, write the result to .opennote/opennote-labels-cache.json with the current timestamp as fetchedAt.
categoryID.category: 0 (no label).When picking a random label, tell the user which label was selected.
Ask the user what they want to write about. Collect:
const now = Date.now(); // Unix milliseconds
const fileName = `${now}.json`;
Build a Quill Delta array of ops, then JSON.stringify() it into the richContent field.
Plain text:
{ "insert": "Hello world" }
Newline (required - every Delta must end with at least one):
{ "insert": "\n" }
Inline styles (on text insert):
bold (boolean)italic (boolean)underline (boolean)strike (boolean)color (hex string like "#2a7fff") — use the palette below for best resultssize (number, typically 2-99; app default is 17){ "insert": "Important", "attributes": { "bold": true, "color": "#ff0000", "size": 20 } }
Available color palette (36 colors):
Always store the light-mode hex — the app remaps it automatically when rendering in dark mode.
| Name | Light-mode hex |
|---|---|
| Black | #000000 |
| Charcoal | #545454 |
| Dark Grey | #616161 |
| Warm Slate | #455a64 |
| Crimson | #b71c1c |
| Deep Red | #c0392b |
| Brick | #bf360c |
| Dark Amber | #a04000 |
| Mustard | #9a6e00 |
| Dark Olive | #558b2f |
| Forest Green | #2e7d32 |
| Deep Gold | #8b6914 |
| Dark Teal | #00695c |
| Dark Cyan | #00838f |
| Royal Blue | #1565c0 |
| Blue | #007aff |
| Indigo | #5856d6 |
| Deep Purple | #6a1b9a |
| Magenta | #e91e63 |
| Pink | #ff2d55 |
| Berry | #880e4f |
| Plum | #6a0572 |
| Dark Violet | #4a148c |
| Brown | #8d6e63 |
| Red | #ff3b30 |
| Dark Rose | #ad1457 |
| Terracotta | #8b3a1f |
| Sea Green | #007a63 |
| Midnight | #1a237e |
| Deep Sage | #2e5902 |
| Ochre | #7a5c00 |
| Steel Grey | #37474f |
| Wine | #8b0000 |
| Mahogany | #6d2f1f |
| Deep Sea | #005b72 |
| Dark Coffee | #4e342e |
Block/line styles (attached to the newline op after the line):
align: "left", "center", "right"indent: integerlist: "ordered", "bullet", "checked", "unchecked"code-block: trueblockquote: true{ "insert": "A bullet point" },
{ "insert": "\n", "attributes": { "list": "bullet" } }
Image embed:
{ "insert": { "image": "{\"src\":\"my_photo.jpg\",\"w\":1920,\"h\":1080}" } }
If dimensions are unknown:
{ "insert": { "image": "my_photo.jpg" } }
Collage embed (multi-image layout):
{
"insert": {
"collage": "{\"layout\":\"twoHorizontal\",\"images\":[\"img1.jpg\",\"img2.jpg\"]}"
}
}
Available collage layouts: twoHorizontal (2), bigLeft2Right (3), bigRight2Left (3), threeRow (3), bigLeft2RectRight (3), bigTop2Bottom (3), bigLeftTopRect2Bottom (4), grid2x2 (4)
Divider embed:
{ "insert": { "divider": "split" } }
The API field richContent must be a JSON string (not an object):
"[{\"insert\":\"My Title\\n\",\"attributes\":{\"bold\":true,\"size\":24}},{\"insert\":\"Today was a great day.\\n\"},{\"insert\":{\"image\":\"photo.jpg\"}},{\"insert\":\"\\n\"}]"
content is a plain-text summary used for home screen preview and search. Strip all formatting — just include the text. Do NOT put JSON or markdown here.
stickerData is a JSON string (not an object) containing an array of sticker overlay objects. Stickers float on top of the note page and are NOT part of richContent.
Each sticker object:
{
"id": "unique_hex_id",
"assetPath": "assets/stickers/bunny.svg",
"dx": 150.0,
"dy": 200.0,
"normalizedDx": 0.39,
"normalizedDy": 0.25,
"scale": 1.0,
"rotation": 0.0
}
Field rules:
id: unique string, format <hex_timestamp>_<6char_hex_random>assetPath: must be an exact match from the bundled sticker list belowdx, dy: pixel offsets from top-left of note canvas (typical canvas ~390px wide)normalizedDx, normalizedDy: proportional position (0.0–1.0 for dx; dy can exceed 1.0 for long content)scale: 0.3 to 4.0 (1.0 = default 70px base size)rotation: radiansExample stickerData field value:
"[{\"id\":\"18f0c9d2_a1b2c3\",\"assetPath\":\"assets/stickers/bunny.svg\",\"dx\":150.0,\"dy\":200.0,\"normalizedDx\":0.39,\"normalizedDy\":0.25,\"scale\":1.0,\"rotation\":0.0}]"
Kawaii:
assets/stickers/backpack.svg assets/stickers/bird.svg
assets/stickers/book.svg assets/stickers/bunny.svg
assets/stickers/burger.svg assets/stickers/cake_slice.svg
assets/stickers/cat.svg assets/stickers/cheese.svg
assets/stickers/chicken.svg assets/stickers/clapboard.svg
assets/stickers/crown.svg assets/stickers/flower_2.svg
assets/stickers/frog.svg assets/stickers/garland.svg
assets/stickers/gramophone.svg assets/stickers/hat.svg
assets/stickers/heart_2.svg assets/stickers/kiwi.svg
assets/stickers/kitten.svg assets/stickers/little_girl.svg
assets/stickers/little_girl_2.svg assets/stickers/meals.svg
assets/stickers/muffin.svg assets/stickers/piggy_bank.svg
assets/stickers/pizza.svg assets/stickers/plant.svg
assets/stickers/pop_corn.svg assets/stickers/rabbit.svg
assets/stickers/rabbit_2.svg assets/stickers/saturn.svg
assets/stickers/shooting_star.svg assets/stickers/skirt.svg
assets/stickers/soda_can.svg assets/stickers/strawberry.svg
assets/stickers/symbol.svg assets/stickers/tea_pot.svg
assets/stickers/ufo.svg assets/stickers/wallet.svg
assets/stickers/watermelon.svg assets/stickers/watermelon_2.svg
Animals: assets/stickers/animals/001-crocodile.svg through 040-hippopotamus.svg (040 total)
Nature: assets/stickers/nature/001-sunflower.svg through 024-tree.svg (024 total)
Characters: assets/stickers/characters/girl_001-girl.svg through girl_020-girl.svg, hippie_001-hippie.svg through hippie_020-dj.svg
Food: assets/stickers/food/misc_001-meat.svg through misc_020-dolphin.svg, k760_001-cat.svg through k760_020-ice_cream.svg, k791_001-cat.svg through k791_020-book.svg
Cute Life: assets/stickers/cute_life/k678_001-badge.svg through k678_020-vinyl_record.svg, k612_001-teddy_bear.svg through k612_020-yogurt.svg, k155_001-egg_and_bacon.svg through k155_020-rainbow.svg
Everyday: assets/stickers/everyday/k448_001-cake.svg through k448_020-violin.svg, k450_001-backpack.svg through k450_020-turtle.svg
If the note includes images, upload each one first:
curl -s -X POST "https://api.opennote.cc/api/v1/images" \
-H "Authorization: Bearer $OPENNOTE_API_TOKEN" \
-F "image=@/path/to/photo.jpg"
Response:
{ "imageName": "photo.jpg", "sizeInMB": 0.42 }
Use the returned imageName in imageList, richContent image embeds, and optionally diaryCoverImageName.
Image constraints:
curl -s -X POST "https://api.opennote.cc/api/v1/diaries" \
-H "Authorization: Bearer $OPENNOTE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fileName": "<timestamp>.json",
"time": <timestamp>,
"content": "<plain text summary>",
"richContent": "<JSON string of Quill Delta ops>",
"stickerData": "<JSON string of sticker array or null>",
"diaryCoverImageName": null,
"category": <categoryID>,
"title": "<optional title>",
"imageList": [],
"isDeleted": false,
"lastModified": <timestamp>,
"hideTitle": false
}'
If the API returns 409 ALREADY_EXISTS, retry with PUT:
curl -s -X PUT "https://api.opennote.cc/api/v1/diaries/<fileName>" \
-H "Authorization: Bearer $OPENNOTE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "content": "...", "richContent": "...", "lastModified": <now> }'
After a successful create or update, append an entry to .opennote/opennote-history.json.
Read the existing file first (or start with an empty array [] if it doesn't exist), then append and write back.
Each history entry format:
{
"fileName": "1741111111111.json",
"action": "created",
"timestamp": 1741111111111,
"title": "Morning Walk",
"contentPreview": "Woke up early and went for a walk in the park...",
"categoryID": 2,
"categoryName": "Personal",
"hasStickers": true,
"hasImages": false,
"imageList": []
}
Rules for the history:
action: either "created" or "updated"contentPreview: first 150 characters of the plain text contentcategoryName: look up from cached labels, or "No Label" if category is 0hasStickers: true if stickerData was non-nullhasImages: true if imageList is non-emptyaction: "updated" (do not overwrite previous entries)Tell the user:
When the user asks to read, search, or use an existing note as a template, use the read endpoints (requires diaries:read scope on the token).
Note: The
diaries:readscope may not yet be available when creating tokens from the app. If you get a403 INSUFFICIENT_SCOPEerror on read endpoints, the token doesn't have this scope. Writing notes and fetching labels will still work.
curl -s "https://api.opennote.cc/api/v1/diaries?category=CATEGORY_ID&search=KEYWORD&limit=50&offset=0" \
-H "Authorization: Bearer $OPENNOTE_API_TOKEN"
Query parameters (all optional):
category — filter by categoryID (integer)search — search in content and title (partial match)limit — results per page, 1–200, default 50offset — pagination offset, default 0Response:
{
"diaries": [ { "fileName": "...", "time": ..., "content": "...", "richContent": "...", ... } ],
"total": 42,
"limit": 50,
"offset": 0
}
curl -s "https://api.opennote.cc/api/v1/diaries/FILENAME.json" \
-H "Authorization: Bearer $OPENNOTE_API_TOKEN"
When the user asks about notes they've previously written (e.g., "what did I write last time?", "find my note about X"), read .opennote/opennote-history.json. You can match by:
title (partial match)contentPreview (keyword search)categoryName (label name)timestamp (date range)fileName (exact match)Report matching entries with their title, content preview, date, and label.
Before sending, validate:
richContent must be either JSON null or a JSON string that parses into an array where every element has an insert key."NULL", "null", or empty string "" into richContent.content must be plain text (for preview/search), not JSON.imageList must contain filename-only entries and include all image filenames from richContent embeds.time and lastModified.stickerData must be either omitted, null, or a JSON string parsing to an array of valid sticker objects.assetPath from the bundled sticker list above.scale should be between 0.3 and 4.0.