一键导入
build-state-change
Implements an emmett state-change slice (command handler, tests, route) from a slice.json definition
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Implements an emmett state-change slice (command handler, tests, route) from a slice.json definition
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Teaches an agent everything about the eventmodelers platform API — all endpoints, their purpose, request payloads, response shapes, authentication, and element types.
Implements an emmett state-view slice (projection, tests, route, migration) from a slice.json definition
Teaches an agent everything about the eventmodelers platform API — all endpoints, their purpose, request payloads, response shapes, authentication, and element types.
Teaches an agent everything about the eventmodelers platform API — all endpoints, their purpose, request payloads, response shapes, authentication, and element types.
Implement automation and translation slices the Cratis way — an IReactor that observes events and produces side effects, triggering further writes via ICommandPipeline. Use when: (1) implementing a new automation / reactor in a Cratis project, (2) a slice.json has a non-empty processors[] section or sliceType === "TRANSLATION", (3) the user provides an Event Modeling artifact or description of an event-driven reaction and asks to implement it, (4) the user says "implement", "create", "add" an automation, reactor, translation, or event-to-command flow in a Cratis Arc / Chronicle project.
Implement Event Sourcing write slices the Cratis way — using Cratis Arc (CQRS) + Cratis Chronicle (event sourcing) in a .NET / C# project. A write slice is: Command → Handle() → Event(s), with optional validators, constraints, and DCB business rules. Use when: (1) implementing a new write slice / command in a Cratis project, (2) a slice.json has a non-empty commands[] / events[] section, (3) the user provides an Event Modeling artifact, specification, or natural-language description of a command and asks to implement it, (4) the user says "implement", "create", "add" a write slice, command, or state change in a Cratis Arc / Chronicle project.
| name | build-state-change |
| description | Implements an emmett state-change slice (command handler, tests, route) from a slice.json definition |
Before doing anything else, read the slice definition from
.slices/{Context}/{slicename}/slice.json. This file is the source of truth for all fields, events, and metadata. Never invent fields not defined there.
A state-change slice processes a command using event sourcing. It:
evolve)decide)From the slice definition, extract:
[Context]Events.ts)Comments & description: Each element (commands, events, readmodels, processors, screens, tables) carries a
comments: string[]array (board comments on that node) and adescriptionfield. The slice itself also hascomments: string[]. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment:POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve(get comment IDs first via GET on the same path without the last two segments).
Each context has one [Context]Events.ts file that exports a union of all event types.
File location: src/slices/{context}/[Context]Events.ts (or wherever the existing one lives — search for it).
import type {Event} from '@event-driven-io/emmett';
type CommonMeta = {
stream_name?: string;
userId?: string;
correlation_id?: string;
causation_id?: string;
};
export type {EventName} = Event<'{EventName}', {
// data fields from slice.json
}, CommonMeta>;
// Add the new event to the union
export type {Context}Events = /* existing events */ | {EventName};
Add each new event type and update the union. Do NOT remove existing types.
{SliceName}Command.tsFile: src/slices/{context}/{SliceName}/{SliceName}Command.ts
import type {Command} from '@event-driven-io/emmett';
import {CommandHandler} from '@event-driven-io/emmett';
import {type {Context}Events} from '../{Context}Events';
import {findEventstore} from '../../../common/loadPostgresEventstore';
// 1. Command type — data fields come from slice.json commands[]
export type {SliceName}Command = Command<'{SliceName}', {
id: string;
// ... other data fields from the slice definition
}, {
correlation_id?: string;
causation_id?: string;
}>;
// 2. State — only fields needed for validation
export type {SliceName}State = {
// e.g. { processed: boolean } or { assignedIds: Set<string> }
// Use {} if no validation state is needed
};
export const {SliceName}InitialState = (): {SliceName}State => ({
// initial values
});
// 3. Evolve — pure function, updates state from past events
export const evolve = (
state: {SliceName}State,
event: {Context}Events,
): {SliceName}State => {
const {type} = event;
switch (type) {
case '{EmittedEventName}':
return {...state, /* update field */};
default:
return state;
}
};
// 4. Decide — validates command, returns events or throws
export const decide = (
command: {SliceName}Command,
state: {SliceName}State,
): {Context}Events[] => {
// idempotency / business rule check
if (state.processed) {
throw {code: 'already_processed', message: 'Already processed'};
}
return [{
type: '{EmittedEventName}',
data: {
id: command.data.id,
// ... map all fields from command.data per slice.json
},
metadata: {
correlation_id: command.metadata?.correlation_id,
causation_id: command.metadata?.causation_id,
userId: command.data.userId,
},
}];
};
// 5. CommandHandler + exported handle function
const {SliceName}CommandHandler = CommandHandler<{SliceName}State, {Context}Events>({
evolve,
initialState: {SliceName}InitialState,
});
export const handle{SliceName} = async (id: string, command: {SliceName}Command) => {
const eventStore = await findEventstore();
const result = await {SliceName}CommandHandler(
eventStore,
id,
(state: {SliceName}State) => decide(command, state),
);
return {
nextExpectedStreamVersion: result.nextExpectedStreamVersion,
lastEventGlobalPosition: result.lastEventGlobalPosition,
};
};
| Scenario | State shape |
|---|---|
| Simple create-once | { created: boolean } |
| Idempotency by user | { processedUserIds: Set<string> } |
| Count validation | { count: number; limit: number } |
| No validation needed | {} (empty object) |
{SliceName}.test.tsFile: src/slices/{context}/{SliceName}/{SliceName}.test.ts
Use DeciderSpecification for unit tests. Derive test scenarios from specifications[] in the slice.json.
import {DeciderSpecification} from '@event-driven-io/emmett';
import {
{SliceName}Command,
{SliceName}InitialState,
decide,
evolve,
} from './{SliceName}Command';
import {describe, it} from 'node:test';
describe('{SliceName} Specification', () => {
const given = DeciderSpecification.for({
decide,
evolve,
initialState: {SliceName}InitialState,
});
it('spec: {SliceName} - creates event on empty stream', () => {
const command: {SliceName}Command = {
type: '{SliceName}',
data: {
id: 'test-id',
// ... test values
},
metadata: {},
};
given([])
.when(command)
.then([{
type: '{EmittedEventName}',
data: {
id: 'test-id',
// ... expected event data
},
metadata: {},
}]);
});
it('spec: {SliceName} - throws when already processed', () => {
const command: {SliceName}Command = {
type: '{SliceName}',
data: {id: 'test-id'},
metadata: {},
};
given([{
type: '{EmittedEventName}',
data: {id: 'test-id'},
metadata: {},
}])
.when(command)
.thenThrows();
});
});
Add one test per specification in the slice.json. If the spec has no precondition events, use given([]).
routes.tsFile: src/slices/{context}/{SliceName}/routes.ts
Concrete example:
src/slices/example/routes.ts— shows the full pattern withrequireUser,assertNotEmpty, error mapping, and OpenAPI annotations. Read it before implementing.
import {Request, Response, Router} from 'express';
import {WebApiSetup} from '@event-driven-io/emmett-expressjs';
import {requireUser} from '../../../supabase/requireUser';
import {{SliceName}Command, handle{SliceName}} from './{SliceName}Command';
export const api = (): WebApiSetup => (router: Router): void => {
router.post('/api/{slicename}/:id', async (req: Request, res: Response) => {
const auth = await requireUser(req, res);
if (auth.error) return;
const id = req.params.id;
const correlationId = req.header('correlation_id') ?? id;
try {
const command: {SliceName}Command = {
type: '{SliceName}',
data: {
id,
// ... map from req.body
},
metadata: {
correlation_id: correlationId,
causation_id: id,
},
};
const result = await handle{SliceName}(id, command);
res.set('correlation_id', correlationId);
res.set('causation_id', id);
return res.status(201).json({
ok: true,
next_expected_stream_version: result.nextExpectedStreamVersion?.toString(),
last_event_global_position: result.lastEventGlobalPosition?.toString(),
});
} catch (err: any) {
const errorMessage = errorMapping(err?.code);
if (errorMessage) {
return res.status(409).json({error: errorMessage});
}
console.error(err);
return res.status(500).json({ok: false, error: 'Server error'});
}
});
};
const errorMapping = (code: string): string | null => {
switch (code) {
case 'already_processed': return 'This action has already been performed.';
// add other error codes from slice.json specifications
default: return null;
}
};
Find the application's router registration (usually src/index.ts or src/app.ts) and add:
import {api as {SliceName}Api} from './slices/{context}/{SliceName}/routes';
// inside the router setup:
{SliceName}Api()(router);
command.metadata?.correlation_id (metadata may be absent in tests)throw {code: 'snake_case_code', message: '...'} — routes catch by err?.codehandle{SliceName}(id, command) — the stream is {context}-{id}src/slices/{context}/{SliceName}/
├── {SliceName}Command.ts ← command handler (decide/evolve/handle)
├── {SliceName}.test.ts ← DeciderSpecification tests
└── routes.ts ← Express POST endpoint
src/slices/{context}/
└── {Context}Events.ts ← add new event types here (update union)
Before marking this slice as Done, verify the implementation against slice.json:
commands[].data has a corresponding field in the Command type — no invented fields, none missingevents[] has a corresponding type in {Context}Events.ts — names match exactlyspecifications[] maps to a test case in {SliceName}.test.tsdescription or comments