Skip to content

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.

null
AnnotationWhat it stands forYou choose it when
SubjectThe center of the catalog: a customer, a person, a version, a market, a product modelYou need a single place that owns a family of setups
ContextOne parallel setup of that subject: an environment, a legal entity, a release, a trim, a cellThe same subject runs in more than one world
ResponsibilityA capability: a module, a service, a subsystem, a part familyYou ship it, configure it, or watch it as its own thing
UnitA job, event, or pipeline inside a responsibilityYou need a smaller name than the whole capability
UsageThis subject consumes this responsibilityThe commercial or entitlement facts belong to the pair
ExecutionThat usage, running in one contextThe facts belong to one concrete setup
Unit of executionOne unit inside one executionA 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 are time (a schedule), message (a message arrives), and volume (a file or another source changes). file and event are 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.

TypeCodeKey
Responsibilityrstrst.{responsibility}
Unituntunt.{responsibility}.{unit}
Subjectsbtsbt.{subject}
Usageusgusg.{subject}.{responsibility}
Contextcntcnt.{subject}.{context}
Executionexeexe.{subject}.{responsibility}.{context}
Unit of executionuxeuxe.{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 @:

text
@house.main.default.exe.northwind.invoicing.production

house, 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:

FieldUse it for
title, descriptionA human name and a short explanation
documentationLinkA deeper page, a runbook, or a contract
labelsFree tags: billing, beta, pci, eu
valuesA small JSON object of facts other configuration may read
isDisabledThe record stays in the catalog, out of effect
validFrom, expiresAtA trial, a subscription window, a version that retires on a date
timeZoneThe 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.

text
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.

text
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.

Released under the MIT License.