This skill should be used when adding new ActivityPub/JSON-LD vocabulary types to the @fedify/vocab package, defining new YAML vocabulary schema files, or updating existing vocabulary definitions in Fedify. Applies when implementing FEPs, extending ActivityStreams vocabulary, or adding third-party vocab types such as Mastodon extensions, Litepub types, or other fediverse vocabularies.
To add a new vocabulary type to the @fedify/vocab package, create a YAML
definition file in packages/vocab/src/, then run code generation.
Vocabulary definitions must always be reviewed carefully by a human before being merged. Errors in vocabulary definitions are difficult to fix after release because they break wire compatibility with existing software in the fediverse. Specifically, verify:
defaultContext is complete and all terms compact correctly (see
“Ensuring Complete Compaction Coverage” below)entity flag is correct — getting this wrong changes the entire
async/sync interface contractfunctional flag is correct — marking a multi-valued property as
functional silently drops valuesrange entry is accurate — wrong range types produce incorrect
TypeScript typesDo not rely solely on automated checks (mise run check)—they verify only
TypeScript compilation, not semantic correctness of the vocabulary.
packages/vocab-runtime/src/contexts.tspackages/vocab/src/<typename-lowercase>.yamlpackages/vocab-runtime/src/contexts.tsmise run codegen to generate TypeScript classesmise run check to verify everything compilesThe generated TypeScript class is automatically exported from @fedify/vocab
via packages/vocab/src/vocab.ts (generated) and packages/vocab/src/mod.ts.
Every YAML file must begin with the schema reference:
$schema: ../../vocab-tools/schema.yaml
| Field | Required | Description |
|---|---|---|
name | yes | TypeScript class name (PascalCase, e.g. Note) |
uri | yes | Fully qualified RDF type URI |
entity | yes | true for entity types (async property accessors); false for value types (sync accessors). Must be consistent across the inheritance chain. |
description | yes | JSDoc string. May use {@link ClassName} for cross-references. |
properties | yes | Array of property definitions (can be empty []) |
compactName | no | Short name in compact JSON-LD (e.g. "Note"). Omit if the type has no compact representation. |
extends | no | URI of the parent type. Omit for root types. |
typeless | no | If true, @type is omitted when serializing to JSON-LD. Used for anonymous structures like Endpoints or Source. |
defaultContext | no | JSON-LD context used by toJsonLd(). See below. |
Entity vs. value type (entity flag):
entity: true — property accessors are async and can fetch remote objectsentity: false — property accessors are synchronous; used for embedded value
objects (e.g. Endpoints, Source, Hashtag)defaultContext formatThe defaultContext field specifies the JSON-LD @context written when
toJsonLd() is called. It can be:
A single context URL:
defaultContext: "https://www.w3.org/ns/activitystreams"
An array of URLs and/or embedded context objects:
defaultContext:
- "https://www.w3.org/ns/activitystreams"
- "https://w3id.org/security/data-integrity/v1"
- toot: "http://joinmastodon.org/ns#"
Emoji: "toot:Emoji"
sensitive: "as:sensitive"
featured:
"@id": "toot:featured"
"@type": "@id"
Embedded context entries are YAML mappings where:
"prefix:term" or "https://..." defines a simple term alias"@id" and optionally "@type": "@id" defines a term that
should be treated as an IRI (linked resource)The defaultContext must cover every term that appears in the JSON-LD
document produced by toJsonLd(), including:
The type's own compactName — if the type has a compactName, the
context must map that name to the type's URI.
All own property compactNames — every property defined directly on this
type must have its compactName (or full URI fallback) resolvable via the
context.
Inherited properties — properties from parent types are usually covered
by the parent's context URL (e.g., https://www.w3.org/ns/activitystreams
covers all core ActivityStreams properties). Verify that the parent's
context URL is included.
Properties of embedded types — when a property's value is an object type that is serialized inline (not just referenced by URL), the context must also cover all of that embedded type's properties. This is the most commonly missed case.
Common embedded types and the context URLs that cover them:
| Embedded type | Context URL to include |
|---|---|
DataIntegrityProof (from proof) | https://w3id.org/security/data-integrity/v1 |
Key (from publicKey) | https://w3id.org/security/v1 |
Multikey (from assertionMethod) | https://w3id.org/security/multikey/v1 |
DidService (from service) | https://www.w3.org/ns/did/v1 |
PropertyValue (from attachment) | schema.org terms in embedded context |
Redundant property compactNames — if a property has
redundantProperties, all their compactNames must also be defined in the
context.
Practical rule: look at an existing type with similar embedded relationships
as a reference. For example, Note and Article include
"https://w3id.org/security/data-integrity/v1" because they embed
DataIntegrityProof objects via the proof property. Person additionally
includes security and DID contexts because it embeds Key, Multikey, and
DidService objects inline.
Omitting a required context causes silent compaction failure: the property
appears in expanded form ("https://example.com/ns#term": [...]) rather than
compact form ("term": ...) in the output.
Each entry in properties is one of two kinds:
Generates get<PluralName>() async iterable and optionally a singular accessor.
- pluralName: attachments # accessor: getAttachments() / attachments
singularName: attachment # used if singularAccessor: true
singularAccessor: true # also generate getAttachment() / attachment
compactName: attachment # JSON-LD compact key
uri: "https://www.w3.org/ns/activitystreams#attachment"
description: |
Identifies a resource attached or related to an object.
range:
- "https://www.w3.org/ns/activitystreams#Object"
- "https://www.w3.org/ns/activitystreams#Link"
Required: pluralName, singularName, uri, description, range
Optional: singularAccessor (default false), compactName, subpropertyOf,
container ("graph" or "list"), embedContext, untyped
Generates a single get<SingularName>() / <singularName> accessor.
- singularName: published
functional: true
compactName: published
uri: "https://www.w3.org/ns/activitystreams#published"
description: The date and time at which the object was published.
range:
- "http://www.w3.org/2001/XMLSchema#dateTime"
Required: singularName, functional: true, uri, description, range
Optional: compactName, subpropertyOf, redundantProperties, untyped,
embedContext
When a property has equivalent URIs from multiple vocabularies, use
redundantProperties to write all aliases on serialization and try them in
order on deserialization:
- singularName: quoteUrl
functional: true
compactName: quoteUrl
uri: "https://www.w3.org/ns/activitystreams#quoteUrl"
redundantProperties:
- compactName: _misskey_quote
uri: "https://misskey-hub.net/ns#_misskey_quote"
- compactName: quoteUri
uri: "http://fedibird.com/ns#quoteUri"
description: The URI of the quoted ActivityStreams object.
range:
- "fedify:url"
embedContext fieldUse embedContext when a nested object should carry its own @context (e.g.,
proof graphs in Data Integrity):
embedContext:
compactName: proof # key under which the context is embedded
inherit: true # use the same context as the enclosing document
untyped fieldWhen untyped: true, the serialized value will not have a @type field.
Requires exactly one type in range. Used for embedded anonymous structures:
- singularName: source
functional: true
compactName: source
uri: "https://www.w3.org/ns/activitystreams#source"
description: The source from which the content markup was derived.
untyped: true
range:
- "https://www.w3.org/ns/activitystreams#Source"
| Range URI | TypeScript type |
|---|---|
http://www.w3.org/2001/XMLSchema#string | string |
http://www.w3.org/2001/XMLSchema#boolean | boolean |
http://www.w3.org/2001/XMLSchema#integer | number |
http://www.w3.org/2001/XMLSchema#nonNegativeInteger | number |
http://www.w3.org/2001/XMLSchema#float | number |
http://www.w3.org/2001/XMLSchema#anyURI | URL (stored as @id) |
http://www.w3.org/2001/XMLSchema#dateTime | Temporal.Instant |
http://www.w3.org/2001/XMLSchema#duration | Temporal.Duration |
http://www.w3.org/1999/02/22-rdf-syntax-ns#langString | LanguageString |
| Range URI | TypeScript type |
|---|---|
https://w3id.org/security#cryptosuiteString | "eddsa-jcs-2022" |
https://w3id.org/security#multibase | Uint8Array |
| Range URI | TypeScript type | Notes |
|---|---|---|
fedify:langTag | Intl.Locale | BCP 47 language tag as plain string |
fedify:url | URL | URL stored as @value (not @id) |
fedify:publicKey | CryptoKey | PEM SPKI-encoded public key |
fedify:multibaseKey | CryptoKey | Multibase-encoded key (Ed25519) |
fedify:proofPurpose | "assertionMethod" | "authentication" | ... | Proof purpose string |
fedify:units | "cm" | "feet" | "inches" | "km" | "m" | "miles" | Place units |
Any uri from another YAML vocabulary file can be used as a range. The
TypeScript type will be the corresponding generated class (e.g.,
"https://www.w3.org/ns/activitystreams#Object" → Object).
When defaultContext references a URL not already in
packages/vocab-runtime/src/contexts.ts, add it to preloadedContexts.
Check existing keys in that file first by searching for the URL. If missing, fetch the actual context document from its canonical URL and add an entry:
"https://example.com/ns/v1": {
"@context": {
// ... paste actual context content here ...
},
},
The keys of preloadedContexts must match the URL strings used in YAML
defaultContext fields. This enables offline JSON-LD processing.
$schema: ../../vocab-tools/schema.yaml