Shared lexicon schemas for comics on AT Protocol.
README.md

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. subject is always the chapter or strip; page and panel narrow it. Replies set parent to the comment they reply to and keep its subject, page and panel, 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.

  • x and y are in ten-thousandths of the image (0–10000, origin top left), like bounds, 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. createdAt is the publish date. For older work, use the original date.
  • Publish a chapter in one com.atproto.repo.applyWrites commit: 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 status to ongoing. Each new page is one commit: the page plus the updated pages list. Set status to completed when 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. Opacity 0 shows the page, 100 hides it.
  • mask: black and white PNG of the panel's exact shape, covering its bounds. Needs bounds.
  • transition: none (default), pan, fade or zoom. 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 alt on 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.