Skip to content

Your first capability

A capability is one file in src/capabilities/ that owns records, decides, and carries its examples. Never hand-write the boilerplate: generate it.

Terminal window
npx atlas new loyalty-points --dir src/capabilities

atlas new writes src/capabilities/loyalty-points.ts and prints the next steps. In an app the file imports @ts-atlas/core/runtime; inside the Atlas repository it imports ../runtime/index.ts. The generated file is a working capability with one command and three examples:

import { capability, t } from '@ts-atlas/core/runtime';
// loyalty-points: one file owns its records, its decisions and the examples that specify them.
// Replace the TODOs, then run: node bin/atlas.ts check && node bin/atlas.ts test
export default capability({
name: 'loyalty-points',
intent: 'TODO: one sentence on what loyalty-points decides and which rule it protects.',
owns: { 'loyalty-points': t.object({ id: t.string, createdAt: t.string }) },
emits: { LoyaltyPointsCreated: t.object({ id: t.string }) },
// sends: { SomeCommand: t.object({ ... }) }, // commands this capability sends to others
})
.command('CreateLoyaltyPoints', {
access: 'user', // who may dispatch it: 'public' | 'user' | 'internal' | ['role', ...]
input: t.object({ id: t.string }),
reads: { existing: { from: 'loyalty-points', key: (input) => input.id } },
decide({ input, state, now }, d) {
if (state.existing !== null) return d.reject('already-exists', { id: input.id });
return d.accept({
write: [d.write('loyalty-points', input.id, { id: input.id, createdAt: now })],
emit: [d.fact('LoyaltyPointsCreated', { id: input.id })],
});
},
examples: [
{ name: 'creates a new loyalty-points', state: { existing: null }, input: { id: 'x1' }, expect: { emits: ['LoyaltyPointsCreated'], writes: ['loyalty-points/x1'] } },
{ name: 'rejects a duplicate id', state: { existing: { id: 'x1', createdAt: '2026-01-01T00:00:00.000Z' } }, input: { id: 'x1' }, expect: { rejects: 'already-exists' } },
{ name: 'rejects an anonymous caller', actor: null, input: { id: 'x1' }, expect: { rejects: 'unauthenticated' } },
],
});
// React to other capabilities' facts with:
// .on('SomeFact', { needs: t.object({ ...only the fields you use }), decide({ fact, state, now }, d) { ... }, examples: [...] })

Fill intent, owns, emits, each command’s access, the decide function and its examples. To use another capability’s data, react to its fact with .on('Fact', { needs }), or declare a foreign read with a shape listing only the fields you use. Never import another capability.

The rules a decision follows:

  • It is pure: no Date, Math.random, fetch, process or timers. Time comes from now in the context; ids come from the input.
  • It returns d.reject(code, params) with a constant kebab-case code, or d.accept({ write, emit, send, remove, schedule }).
  • Identity is ctx.actor, never a payload field. Ownership is checked against the records you read.
Terminal window
npx atlas check --dir src/capabilities

Every diagnostic says what rule broke, where, why it matters, and the exact fix. Apply each fix: line literally and re-run until it prints:

✓ system is consistent (0 errors, 0 warnings)

See check rules for every code.

Terminal window
npx atlas test --dir src/capabilities

atlas test runs every handler and query example. Examples are the spec: documentation for people and tests for machines, in the same place.

src/capabilities/orders.ts in the repository is the canonical example: two commands, versioned reads, an ownership check against the record, and examples for every rejection. Read it next to Capabilities.