Annotations
Annotations are how Storyteller describes a product. A handful of named records is enough to answer the questions that usually live in people's heads, in wikis, and in deployment repos that have drifted apart:
- Who consumes this capability?
- In which setup?
- With which configuration?
- Which job runs for them, and when?
The same records are the subscription catalog. A service asks Storyteller for the annotation it is about to run, reads the effective configuration, and behaves accordingly. New people on the team read the same catalog. An incident starts from a key, and the key already says which customer, which capability, and which setup are involved.
TIP
Seven annotation types cover a SaaS product, a consumer suite, a large distributed system, a monolith that still has several versions in production, and a physical product. The examples later in this guide use one model for all of them.
The shape
Three annotations are the nouns you choose. The others are the relations Storyteller keeps between them, so each relation can carry its own facts.
| Annotation | What it stands for | You choose it when |
|---|---|---|
| Subject | The center of the catalog: a customer, a person, a version, a market, a product model | You need a single place that owns a family of setups |
| Context | One parallel setup of that subject: an environment, a legal entity, a release, a trim, a cell | The same subject runs in more than one world |
| Responsibility | A capability: a module, a service, a subsystem, a part family | You ship it, configure it, or watch it as its own thing |
| Unit | A job, event, or pipeline inside a responsibility | You need a smaller name than the whole capability |
| Usage | This subject consumes this responsibility | The commercial or entitlement facts belong to the pair |
| Execution | That usage, running in one context | The facts belong to one concrete setup |
| Unit of execution | One unit inside one execution | A job has its own schedule or payload per setup |
Usage, execution, and unit of execution are drawn with a dashed border because they are relations. They are still real records. Each one has a key, metadata, and a configuration document, which is what makes a customer-specific or environment-specific difference addressable.
A subject lists its contexts and the responsibilities it uses. A context lists the responsibilities that run there. A responsibility lists its units. Creating a deeper annotation fills in any missing parent and updates those lists, so the catalog stays linked from both sides.
What each one is for
Subject
The subject is the sun of the model. In a B2B product it is the customer. In a consumer finance suite it is the person. In a desktop product it can be a major version that is still alive. In a vehicle line it is the product model.
Everything you want to ask "for this one" hangs under the subject: its contexts, the responsibilities it uses, and the configuration that should be true for all of them.
Context
A context is an optional second axis on a subject. It names a world in which the subject operates.
Typical contexts are an environment (production, sandbox), a legal entity a person represents, a software version still in production, a trim of a physical product, or a cell of a distributed system. A subject with a single world still has one context, often named production or default, so every running capability has an execution key.
Responsibility
A responsibility is one building block of the product. In a modulith that is usually one satellite. It can just as well be a microservice, a module inside a monolith, an add-on, or a subsystem of a physical product.
The responsibility holds the defaults of the capability itself: the behavior you ship before any customer has a say.
Unit
A unit goes one step below a responsibility. It names a job, a workflow step, a file arrival, or a message the capability handles. Two fields describe it:
unitType— any text. Common kinds aretime(a schedule),message(a message arrives), andvolume(a file or another source changes).fileandeventare used as well.unitDefinition— any text: a Cron expression, a message name rule, template, or source name, or a volume source or path filter
The scheduler actor that fires units is further along the road map. The annotation is already part of the model, so a job can be named, documented, and configured per customer today.
Usage
A usage is the pair (subject, responsibility). It is the right place for facts that are true because this subject consumes this capability, in every context: the plan, the seats, the feature package, the contract.
A subject with three responsibilities has three usages. A responsibility sold to a hundred customers has a hundred usages. Listing usages is the answer to "what does this customer consume?" and "who uses this module?".
Execution
An execution is the triple (subject, responsibility, context). It is the runtime record of a satellite: this capability, for this subject, in this world.
This is where a production setup diverges from a sandbox, where one legal entity gets a different limit, and where one version of a monolith keeps an old flag. The effective configuration of an execution is what a running service should read.
Unit of execution
A unit of execution is one unit inside one execution: this job, for this subject, in this context. A nightly export can keep the responsibility's definition and still override the hour for a single customer in a single environment.
Keys
Every annotation has a key. The key is the address, and it already contains the names of its parents, so you can see the whole relation without a lookup.
| Type | Code | Key |
|---|---|---|
| Responsibility | rst | rst.{responsibility} |
| Unit | unt | unt.{responsibility}.{unit} |
| Subject | sbt | sbt.{subject} |
| Usage | usg | usg.{subject}.{responsibility} |
| Context | cnt | cnt.{subject}.{context} |
| Execution | exe | exe.{subject}.{responsibility}.{context} |
| Unit of execution | uxe | uxe.{subject}.{responsibility}.{context}.{unit} |
A name is one segment. The separator is a dot, so a name itself does not contain a dot. northwind, invoicing, and production are names. exe.northwind.invoicing.production is the execution of the invoicing responsibility for Northwind in the production context.
Inside an organization, a project, and a view, the full address starts with @:
@house.main.default.exe.northwind.invoicing.productionhouse, main, and default are the conventional organization, project, and view names. A second view, such as @house.main.2026-q4.exe.northwind.invoicing.production, is a parallel edition of the same catalog. Views are explained with configuration, because that is where they pay off.
What every annotation can carry
The relation is the structure. These fields are the catalog you can show to a person:
| Field | Use it for |
|---|---|
title, description | A human name and a short explanation |
documentationLink | A deeper page, a runbook, or a contract |
labels | Free tags: billing, beta, pci, eu |
values | A small JSON object of facts other configuration may read |
isDisabled | The record stays in the catalog, out of effect |
validFrom, expiresAt | A trial, a subscription window, a version that retires on a date |
timeZone | The zone in which that window, or a unit's schedule, is interpreted |
isDisabled and the validity window decide whether a record is active. The API answers only active records by default, so a service that lists the executions of a customer, or reads one of them, does not get a disabled, not-yet-valid or expired one by accident. Tools that manage the catalog ask for the others with includeInactive=true, as the admin UI and sform story --inactive do. The catalog keeps the record either way, which is what you want when someone asks why a customer stopped receiving a feature on a given day. Configuration is not filtered: an inactive record still resolves its documents.
values is the bridge into configuration. A resolved document can pull a field from the annotation, or from an ancestor, with an @annotation(...) expression. The customer tier can live on the subject, and every execution can refer to it, without copying the tier into every document.
Deleting and restoring
Deleting an annotation deletes everything below it in the graph: a subject takes its contexts, usages, executions and units of execution with it. Nothing is lost at once. The deleted records leave the catalog and every configuration read, and wait 90 days for a restore. Then they are removed for good.
sform story delete sbt.northwind # delete; prints the date it is purged
sform story deleted # what was deleted, newest first
sform story restore sbt.northwind # bring it back with everything it took
sform story history sbt.northwind # who changed it, when, and what
sform story delete sbt.northwind --permanent # remove it now (Administrator or Owner)A machine deletes and restores only with the scope Annotation.Write and the management scope Annotation.Manage, and never deletes permanently.
Every change is recorded with the account or machine that made it: creates, updates (with the old and new values), deletes, restores, and configuration versions.
Writing a key that is deleted creates it again with an empty configuration. sform story set asks first and can restore the old record instead, so its configuration and history come back.
Export and import
The annotations of a view travel in its export, next to its schemas, templates and configurations. Each annotation is a file named by its key, such as exe.northwind.invoicing.production.json. The file holds what you can edit: title, description, documentation link, labels, values, the disabled flag, the validity window and the time zone, and for a unit its unit type and definition. An import replaces the content, so a field left out of the file is cleared; a unit without its type or definition gets the defaults of a new unit (event, unknown). The key gives the type and the parents, so they are not in the file. The audit and the deleted annotations stay with the platform.
sform story export --include annotations # only the catalog (a user: Administrator or Owner; a machine: Annotation.Read and Configuration.Transfer)
sform story import catalog.zip --dry-run # what an import would change, without writing (a user: Administrator or Owner; a machine: the write scopes of the included kinds, their management scopes such as Annotation.Manage, and Configuration.Transfer)An import creates the missing annotations, ancestors before their descendants, and updates the ones that differ. With --existing skip, it keeps the ones the view has. A deleted annotation in the archive is created again, as any write of a deleted key does: its old configuration does not come back, only the one the archive holds. With skip, it stays deleted. Every write is recorded with the account or machine that ran the import. In the Administration UI, the Export & Import page of the view does the same.
Why this stays small
The model refuses a new entity type for every new business idea. A legal entity, a datacenter, a trim level, and a software version are all contexts. An add-on, a microservice, and a physical subsystem are all responsibilities. The platform stays fast at a large number of these records, including a subject per end customer, because the key shape does not change as the catalog grows.
What changes from product to product is the meaning you assign. How to model a product is a short way to assign it. The examples show the assignment for six different products.
Where configuration sits
Each annotation stores its own configuration document: only the values that belong at that level. Reading an execution returns those documents merged, from the responsibility and the subject down to the execution itself. That merge is the whole point of describing a system with annotations, and it has its own page: Configuration.
42for.net