Sequential.work Lexicons #
Shared lexicon schemas for comics on AT Protocol.
Records live under work.sequential.* and support three formats:
- Vertical: webtoons, read by scrolling. Chapters are made of slices.
- Page: comic books. Chapters are made of pages, divided into panels.
- Strip: newspaper strips. Strips belong directly to their series.
Setup #
Requires Node 22.18+ and pnpm.
pnpm install
pnpm lexicon:emit # TypeScript → JSON schemas in out/
pnpm lexicon:import # JSON schemas → TypeScript
pnpm typecheck
Versioning #
The schema set is versioned in package.json, with changes listed in CHANGELOG.md and each release tagged vX.Y.Z in git.
- Published records exist, so changes must be backward compatible. Bump the minor version for new lexicons, new optional fields, raised caps or new known values, and the patch version for descriptions and docs.
- Removing an optional field is a minor change. Existing records still validate and apps ignore the old value. Never reuse the removed field's name.
- Removing a required field, making an optional field required, renaming a field, lowering a cap or changing a type breaks existing records. Make a new lexicon with a new NSID instead (e.g.
work.sequential.chapterV2).
Concepts #
Record Structure #
Vertical
series (format: vertical)
└── chapters[] → chapter
├── covers[] → cover (first = primary, rest = variants)
└── slices[] → slice
Page
series (format: page)
└── chapters[] → chapter
├── covers[] → cover (first = primary, rest = variants)
└── pages[] → page
└── panels[] (inline)
Strip
series (format: strip)
└── strip (ordered by createdAt)
└── panels[] (inline)
- Parents list their children in order, and children point back to their parent.
- Strips aren't listed on the series, because a long-running strip would outgrow the record.
- Panels are inline, so re-cutting a page's panels is one record update.
- Use DID-form AT-URIs (
at://did:plc:…), never handles. AppViews should resolve handles before writing.
Reader Records #
Readers write these in their own repos.
graph.subscription: follows a series. Delete it to unsubscribe.graph.list: a reading list or reading order of chapters and strips from any creator. AppViews skip items that aren't chapters or strips, or that were deleted.feed.like: likes a chapter or strip. Delete it to unlike.feed.comment: comments on a chapter or strip, or on one of its pages, slices or panels.subjectis always the chapter or strip;pageandpanelnarrow it. Replies setparentto the comment they reply to and keep itssubject,pageandpanel, so AppViews can count a work's comments without walking threads.
A reply to a comment on a chapter:
{
"$type": "work.sequential.feed.comment",
"subject": "at://did:plc:creator/work.sequential.chapter/3kchapter",
"parent": "at://did:plc:reader/work.sequential.feed.comment/3kcomment",
"text": "That last page got me too.",
"lang": "en",
"createdAt": "2026-10-03T12:00:00.000Z"
}
A comment on the fourth panel of a page:
{
"$type": "work.sequential.feed.comment",
"subject": "at://did:plc:creator/work.sequential.chapter/3kchapter",
"page": "at://did:plc:creator/work.sequential.page/3kpage",
"panel": 3,
"text": "This reveal!",
"lang": "en",
"createdAt": "2026-10-08T12:00:00.000Z"
}
panel is an index into panels[], so it can point at a different panel after a re-cut. AppViews ignore indexes past the last panel.
Annotations #
feed.annotation pins localized text to a point on a page, slice or strip. Each annotation is its own record, so a work can have many.
xandyare in ten-thousandths of the image (0–10000, origin top left), likebounds, so they fit every localized image.- Anyone can write an annotation in their own repo. AppViews choose whose to show. To show only the creator's, keep annotations whose repo DID matches the subject's.
{
"$type": "work.sequential.feed.annotation",
"subject": "at://did:plc:creator/work.sequential.page/3kpage",
"x": 6200,
"y": 1450,
"text": [
{ "lang": "en", "text": "The sign in the background is from my hometown." },
{ "lang": "ja", "text": "背景の看板は私の故郷のものです。" }
],
"createdAt": "2026-10-08T12:00:00.000Z"
}
Publishing #
- Repos are public, so drafts and scheduled posts stay in the AppView until published.
createdAtis the publish date. For older work, use the original date. - Publish a chapter in one
com.atproto.repo.applyWritescommit: the chapter, its covers, and its slices or pages. Pick the chapter's record key first so the children can point to it. - To release a chapter a page at a time, set its
statustoongoing. Each new page is one commit: the page plus the updatedpageslist. Setstatustocompletedwhen done.
Localization #
One record holds every language. Text and images are lists tagged with BCP-47 codes:
"title": [
{ "lang": "ja", "text": "星の庭" },
{ "lang": "en", "text": "Garden of Stars" }
]
- Panels, versions and credits are shared, so a translation only adds images and text.
- Apps fall back to the series'
nativeLanguage. - Length limits count graphemes, so CJK text gets the same room as English.
Panels #
Pages and strips can be read panel-by-panel. Every panel field is optional.
bounds: the rectangle the app fits to the viewport.background: color around the panel. Opacity0shows the page,100hides it.mask: black and white PNG of the panel's exact shape, covering itsbounds. Needsbounds.transition:none(default),pan,fadeorzoom. Reversed when reading backward.
{
"bounds": { "x": 1200, "y": 800, "width": 4000, "height": 4000 },
"background": { "r": 0, "g": 0, "b": 0, "a": 100 },
"mask": { "$type": "blob", "mimeType": "image/png", ... },
"transition": "fade",
"alt": [{ "lang": "en", "text": "A round panel of the moon over the city." }]
}
Alt Text #
- Slices and covers have
alton the record. Pages and strips have it on each panel. - Describe the art and transcribe all lettering: dialogue, captions and sound effects.
"alt": [{ "lang": "en", "text": "The hero stands over the defeated monster, bored. HERO: \"OK.\" SFX: THOOM" }]
Labels #
Content warnings are AT Protocol self-labels on the series.
- Standard:
porn,sexual,nudity,graphic-media - Suggested:
profanity,violence,drugs,self-harm,abuse
Apps ignore values they don't recognize.
"labels": {
"$type": "com.atproto.label.defs#selfLabels",
"values": [{ "val": "graphic-media" }, { "val": "profanity" }]
}
Lexicons #
defs#x is short for work.sequential.defs#x. On arrays, (1–100) and (≤ 100) are length limits.
work.sequential.series #
| Property | Type | Req | Notes |
|---|---|---|---|
title |
defs#localizedTitle[] (1–100) |
yes | |
banner |
blob (≤ 4 MiB) | yes | 3:1, e.g. 3000×1000. |
poster |
blob (jpeg/png, ≤ 4 MiB) | yes | Share image. 1.91:1, e.g. 1200×630. |
description |
series#localizedDescription[] (1–100) |
yes | Max 255 graphemes each. |
createdAt |
datetime | yes | |
categories |
defs#category[] (1–3) |
yes | Most relevant first. |
format |
string | yes | vertical, page, strip |
readingDirection |
string | no | ltr (default), rtl |
status |
string | no | ongoing (default), completed, hiatus, cancelled |
nativeLanguage |
language | no | Fallback language. |
labels |
selfLabels |
no | See Labels. |
chapters |
at-uri[] (≤ 5000) | no | Not used by strip. |
work.sequential.chapter #
| Property | Type | Req | Notes |
|---|---|---|---|
series |
at-uri | yes | |
title |
defs#localizedTitle[] (≤ 100) |
no | |
description |
defs#localizedDescription[] (≤ 100) |
no | |
tags |
defs#tag[] (≤ 10) |
no | |
createdAt |
datetime | yes | Publish date. |
status |
string | no | completed (default), ongoing, hiatus, cancelled |
covers |
at-uri[] (1–500) | yes | First is primary. |
slices |
at-uri[] (≤ 1000) | no | Vertical format. |
pages |
at-uri[] (≤ 1000) | no | Page format. |
credits |
defs#credit[] (≤ 100) |
no |
work.sequential.cover #
Different art (Cover B) is its own record. The same art with a different treatment (sketch, virgin) goes in versions.
| Property | Type | Req | Notes |
|---|---|---|---|
chapter |
at-uri | yes | |
label |
string (≤ 100 graphemes) | no | e.g. Cover A, 1:25 Incentive |
images |
defs#localizedImage[] (1–100) |
yes | |
alt |
defs#localizedAlt[] (≤ 100) |
no | |
versions |
defs#version[] (≤ 50) |
no | |
aspectRatio |
defs#aspectRatio |
no | |
credits |
defs#credit[] (≤ 100) |
no | When different from the chapter's. |
work.sequential.slice #
| Property | Type | Req |
|---|---|---|
chapter |
at-uri | yes |
images |
defs#localizedImage[] (1–100) |
yes |
alt |
defs#localizedAlt[] (≤ 100) |
no |
versions |
defs#version[] (≤ 50) |
no |
aspectRatio |
defs#aspectRatio |
no |
work.sequential.page #
| Property | Type | Req | Notes |
|---|---|---|---|
chapter |
at-uri | yes | |
images |
defs#localizedImage[] (1–100) |
yes | |
versions |
defs#version[] (≤ 50) |
no | |
aspectRatio |
defs#aspectRatio |
no | |
span |
integer (≥ 1) | no | Printed pages covered: 1 (default), 2 for a spread, 3+ for a foldout. |
panels |
defs#panel[] (≤ 100) |
no | In reading order. |
work.sequential.strip #
| Property | Type | Req | Notes |
|---|---|---|---|
series |
at-uri | yes | |
title |
defs#localizedTitle[] (≤ 100) |
no | |
tags |
defs#tag[] (≤ 10) |
no | |
createdAt |
datetime | yes | Publish date and sort order. |
images |
defs#localizedImage[] (1–100) |
yes | |
versions |
defs#version[] (≤ 50) |
no | |
aspectRatio |
defs#aspectRatio |
no | |
panels |
defs#panel[] (≤ 100) |
no | In reading order. |
credits |
defs#credit[] (≤ 100) |
no |
work.sequential.graph.list #
| Property | Type | Req | Notes |
|---|---|---|---|
title |
defs#localizedTitle[] (1–100) |
yes | |
description |
defs#localizedDescription[] (≤ 100) |
no | |
createdAt |
datetime | yes | |
items |
at-uri[] (≤ 1000) | no | Chapters or strips, in reading order. |
work.sequential.graph.subscription #
| Property | Type | Req |
|---|---|---|
series |
at-uri | yes |
createdAt |
datetime | yes |
work.sequential.feed.like #
| Property | Type | Req | Notes |
|---|---|---|---|
subject |
at-uri | yes | Chapter or strip. |
createdAt |
datetime | yes |
work.sequential.feed.comment #
| Property | Type | Req | Notes |
|---|---|---|---|
subject |
at-uri | yes | Chapter or strip. Replies keep their parent's. |
page |
at-uri | no | Page or slice of the chapter. Replies keep their parent's. |
panel |
integer (≥ 0) | no | Index into the page's or strip's panels. Replies keep their parent's. |
parent |
at-uri | no | Comment being replied to. |
text |
string (≤ 1000 graphemes) | yes | |
facets |
app.bsky.richtext.facet[] |
no | Mentions, links, hashtags. |
lang |
language | no | |
createdAt |
datetime | yes |
work.sequential.feed.annotation #
| Property | Type | Req | Notes |
|---|---|---|---|
subject |
at-uri | yes | Page, slice or strip. |
x |
integer (0–10000) |
yes | Ten-thousandths of the image width, from the left. |
y |
integer (0–10000) |
yes | Ten-thousandths of the image height, from the top. |
text |
defs#localizedText[] (1–100) |
yes | |
createdAt |
datetime | yes |
work.sequential.defs #
panel: all fields optional. See Panels.
| Property | Type | Notes |
|---|---|---|
bounds |
bounds |
|
alt |
localizedAlt[] (≤ 100) |
|
background |
rgba |
Transparent when omitted. |
mask |
blob (png, ≤ 4 MiB) | |
transition |
string | none (default), pan, fade, zoom |
bounds: x, y, width, height, in ten-thousandths of the image (0–10000, origin top left), so they fit every localized image.
rgba: r, g, b (0–255) and a (opacity, 0–100).
aspectRatio: width, height (≥ 1).
localizedImage: image (blob, jpeg/png/webp/avif, ≤ 4 MiB), lang.
localizedAlt, localizedTitle, localizedDescription, localizedText: lang and text, max 10000, 100, 3000 and 1000 graphemes.
version: another version of the same art.
| Property | Type | Notes |
|---|---|---|
image |
blob (≤ 4 MiB) | |
kind |
string (≤ 64 graphemes) | Stages: thumbnails, layout, roughs, pencils, lineart, inks, flats, colors, letters. Treatments: e.g. sketch, blackAndWhite, virgin. |
credit
| Property | Type | Req | Notes |
|---|---|---|---|
displayName |
string (≤ 100 graphemes) | yes | |
did |
did | no | |
role |
string (≤ 64 graphemes) | yes | writer, penciler, inker, colorist, flats, letterer, cover, creator, editor, editorInChief, associateEditor, assistantEditor, designer, artist, translator, painter, backgrounds |
category: a string, e.g. action, comedy, fantasy, sliceOfLife. Max 64 graphemes.
tag: a string without a leading #. Max 64 graphemes.
All string values listed above are known values, not a closed list. AppViews may add their own and ignore ones they don't recognize.