Authoring Cheat Sheet
Read this once before your first change. One feature = one file under
src/features/. A change to a feature touches exactly two files: the feature
file and its colocated test.
The canonical feature file
src/features/notes.ts — schema, store port, operations, HTTP wiring, errors:
import { command, feature, port, query, s } from "@agentixdev/core";
export const Note = s.object({
id: s.string({ min: 1 }),
title: s.string({ min: 1, trim: true }),
body: s.string(),
});
export type Note = s.Infer<typeof Note>;
export const NoteStorage = port.store("noteStorage", Note);
export const notes = feature("notes", {
operations: {
create: command({
input: Note,
output: Note,
errors: { NOTE_ALREADY_EXISTS: { http: 409, details: { id: s.string() } } },
http: { method: "POST", path: "/notes", status: 201 },
effects: { load: NoteStorage.get, save: NoteStorage.save },
async execute({ input, effects, fail }) {
if (await effects.load(input.id)) return fail("NOTE_ALREADY_EXISTS", { id: input.id });
return effects.save(input);
},
}),
get: query({
input: s.object({ id: s.string({ min: 1 }) }),
output: Note,
errors: { NOTE_NOT_FOUND: { http: 404, details: { id: s.string() } } },
http: { method: "GET", path: "/notes/:id" },
effects: { load: NoteStorage.get },
async execute({ input, effects, fail }) {
return (await effects.load(input.id)) ?? fail("NOTE_NOT_FOUND", { id: input.id });
},
}),
},
});
Rules the runtime and compiler enforce:
- Operation ids are derived:
${featureId}.${key}(herenotes.create,notes.get). executereturns a plain output value orfail(code, details).failis typed against theerrorsmap and RETURNS (never throw it).- Effects are
Port.opNamereferences; only declared effects are callable. Adapters return plain values or throw — a throw becomes anEFFECT_FAILUREfault. - Queries cannot declare write effects or emit events (type + runtime error).
- Object schemas reject unknown keys.
trim: truenormalizes before validation. - Ambient I/O, clocks, randomness,
fetch, and env access are compiler diagnostics insidesrc/features/.
The 2-file change recipe
To add an operation (example: notes.delete):
agentix inspect notes --root . --json --compact— the bounded artifact shows operations, routes, errors, effects, and tests. Do not read the app shell.- Edit
src/features/notes.ts: add onecommand()/query()entry tooperations, declaringinput,output,errors,http,effects.port.storealready providesget/save/delete/list— no new port needed. - Edit
src/features/notes.test.ts: add cases for the success path and each declared error code. agentix verify notes.delete --root .— runs the narrowest safe typecheck + test commands.
src/app.ts does not change: routes are auto-derived from http metadata and
required adapters are derived from operation effects.
Declaring errors and statuses
errors: {
CODE_A: { http: 409, details: { id: s.string() } }, // status + details shape
CODE_B: { http: 404 }, // status, empty details
CODE_C: { details: SomeSchema }, // defaults to HTTP 422
CODE_D: SomeSchema, // bare schema shorthand
},
fail("CODE_A", { id }) returns the declared failure; over HTTP it becomes
{ "ok": false, "error": { "code": "CODE_A", "details": { ... } } } with the
declared status (422 when no http was given).
Ports
port.store(id, objectSchema)— CRUD preset (schema must haveid):get(id) -> record | undefined,save(record) -> record,delete(id) -> boolean,list({}) -> record[], plus.memory()— a built-in Map adapter for the app shell and tests.- Custom port:
port(id, { opName: port.read({ input, output }) })withport.read/write/time/random/external. Expected alternatives (e.g. declined payment) belong in the output schema as a union — port ops have no error channel. - Adapter:
MyPort.adapter({ opName: async (input) => value }).
When app.ts needs touching (almost never)
Only when you introduce a NEW feature or a NEW port:
export const app = createApplication({
features: [notes], // + new feature
adapters: [NoteStorage.memory()], // + adapter for a new port
});
export const handler = createHttpHandler(app);
Everything else — routes, statuses, permissions, effects — lives on the operation descriptor in the feature file.
Verification commands
agentix inspect <feature-or-operation> --root . --json --compact # bounded context
agentix verify <feature-or-operation> --root . # narrow typecheck + tests
agentix scaffold feature <name> --root . # new feature file + test
npm exec -- vitest run src/features/<name>.test.ts
npm exec -- tsc -b . --pretty false
verify exit codes: 0 pass, 1 verification failure, 2 invalid invocation,
3 internal failure. If affected.widened is true in inspect output, run the
workspace-level commands it prints instead of the narrow plan.