Generate TypeScript concept handler ( . handler . ts ) implementation scaffolds from provided configuration including concept name , action signatures , and storage patterns . Defaults to functional ( StorageProgram ) style ; falls back to imperative style only when explicitly requested . Optionally generates a conformance test file
Scaffold a TypeScript handler for concept $ARGUMENTS with typed actions, storage patterns, and a conformance test.
When to use: Use when implementing a concept handler in TypeScript. Generates a .handler.ts handler with register(), typed action methods, input extraction, storage patterns, and a conformance test file.
ok. Do not use domain-specific success names like , , . Domain context belongs in the output fields. Exception: actions with multiple distinct success outcomes that syncs need to distinguish (e.g., / for cache lookup).configuredcreatedregisteredokmissas casts at the top of each method. CRITICAL: validate required fields BEFORE any storage operations. For every error-case fixture in the concept spec (fixtures with -> error, -> invalid, etc.), the handler MUST have a corresponding guard clause that returns the error variant. Common pattern: if (!input.name || (input.name as string).trim() === '') return complete(createProgram(), 'error', { message: 'name is required' }). Empty strings, missing required params, and obviously invalid inputs must be caught early.Self-register with PluginRegistry so KindSystem can track HandlerConfig → HandlerImpl transformations. Registration is also handled automatically by register-generator-kinds.sync.
Examples: Register the handler scaffold generator
const result = await handlerScaffoldGenHandler.register({}, storage);
Dry-run the generation using Emitter content-addressing to classify each output file as new, changed, or unchanged. No files are written.
Arguments: $0 conceptName (string), $1 actions (actiondef[])
Generate a handler implementation with typed action methods , input extraction , and storage patterns . Defaults to functional ( StorageProgram ) style ; pass style = imperative for the imperative fallback . Optionally generates a conformance test .
Arguments: $0 conceptName (string), $1 actions (actiondef[]), $2 style (string)
Checklist:
Examples: Generate a handler
clef scaffold handler --concept User --actions create,update,delete
Generate with test only
clef scaffold handler --concept Article
Refine the generated handler. Default style is FUNCTIONAL (StorageProgram): 1. Each action returns a StorageProgram<A> built with DSL functions: createProgram(), get(), find(), put(), merge(), del(), branch(), complete(), pure(). 2. CRITICAL — put() vs putFrom(): Use put(p, rel, key, value) ONLY when value is fully known at construction time (static data, input parameters). Use putFrom(p, rel, key, (bindings) => value) when value depends on data read from storage via get()/find(). Same applies to merge() vs mergeFrom(), and complete() vs completeFrom(). The bindings parameter is a Record<string,unknown> containing all values bound by prior get()/find()/mapBindings() calls. Never reference a bindAs name as a direct variable — always access it through the bindings object. 3. Use mapBindings(p, (bindings) => derivedValue, 'bindAs') to derive new values from accumulated bindings (e.g. computing a count, filtering, sorting). 4. For external API calls (HTTP, gRPC, GraphQL, WebSocket): use perform(p, 'http', 'POST', { endpoint: 'name', path, body }, 'bindAs'). This routes through the execution layer: EffectHandler → ExternalCall → HttpProvider → instance endpoint. Create a Tier 3 instance provider concept (like OpenAiEndpoint, VercelApiEndpoint) for each specific API. 5. For local computation (ONNX, WASM, shell): use perform(p, 'onnx', 'infer', { session: 'name', inputs }, 'bindAs'). Routes through: EffectHandler → LocalProcess → OnnxProvider/WasmProvider → instance. Create a LocalModelInstance for each model. 6. NEVER use direct fetch(), execSync(), or other I/O. All external/internal calls go through perform() so they get: ConnectorCall tracking, RetryPolicy, CircuitBreaker, RateLimiter, PerformanceProfile, ErrorCorrelation, RuntimeCoverage — automatically via sync wiring. 7. Use branch() for conditionals (not if/else), complete() for terminal values. 8. Lens-based access with getLens(), putLens(), modifyLens() where possible. Imperative style (ConceptHandler with async methods) is ONLY for handlers requiring direct filesystem access (e.g. EmbeddingCache).
| Input | Type | Purpose |
|---|---|---|
| conceptName | String | PascalCase concept name |
| actions | list ActionDef | Action signatures with params and variants |
| inputKind | String | KindSystem input kind |
| outputKind | String | KindSystem output kind |
| capabilities | list String | Generator capabilities |
Handler doesn't return error variant on failure — caller gets an unstructured exception.
Bad:
async create(input, storage) {
const id = crypto.randomUUID();
await storage.put('items', id, { name: input.name });
return { variant: 'ok', item: id };
// Exception propagates raw if storage.put fails!
}
Good:
async create(input, storage) {
try {
const id = crypto.randomUUID();
await storage.put('items', id, { name: input.name });
return { variant: 'ok', item: id };
} catch (err) {
return { variant: 'error', message: String(err) };
}
}
Returning { _variant: 'notfound' } from a completeFrom callback does NOT change the variant. The variant is always the second argument to completeFrom. The _variant field is just ignored data in the output.
Bad:
return completeFrom(p, 'ok', (bindings) => {
const entry = all.find(...);
if (!entry) return { _variant: 'notfound' }; // BROKEN: variant is still 'ok'
return { handler: entry.id };
});
Good:
p = mapBindings(p, (b) => (b.all as any[]).find(...) || null, '_found');
return branch(p,
(b) => !!b._found,
(b) => completeFrom(b, 'ok', (b) => ({ handler: (b._found as any).id })),
(b) => complete(b, 'notfound', {}),
);
Returning { _puts: [{ rel, key, value }] } from a completeFrom callback does NOT write to storage. The interpreter treats completeFrom results as terminal pure values — it never inspects _puts.
Bad:
return completeFrom(p, 'ok', (bindings) => ({
variant: 'ok',
_puts: [{ rel: 'items', key: 'abc', value: record }],
item: id,
}));
Good:
let p2 = putFrom(p, 'items', 'abc', (b) => record);
return completeFrom(p2, 'ok', (b) => ({ item: id }));
putFrom(p, rel, key, fn) requires key to be a static string known at build time. If the key is computed at runtime (e.g., a generated UUID), you MUST use imperative style for that action. Mix functional and imperative via autoInterpret + override.
Bad:
// Cannot use a dynamic key with putFrom
p = putFrom(p, 'items', generatedId, (b) => record);
Good:
const baseHandler = autoInterpret(_handler);
const handler = { ...baseHandler,
async myAction(input, storage) {
const id = generateId();
await storage.put('items', id, record);
return { variant: 'ok', item: id };
},
};
If an action needs to call methods on a stateful class instance (e.g. SyncEngine, Parser), it cannot be expressed as a pure StorageProgram. Use imperative style for those specific actions while keeping other actions functional.
Generate a handler scaffold:
npx tsx cli/src/index.ts scaffold handler --concept User --actions create,update,delete
Run generated conformance test:
npx vitest run tests/user.conformance.test.ts
Generate tests from invariants:
npx tsx cli/src/index.ts test-gen --concept User --language typescript
Run scaffold generator tests:
npx vitest run tests/scaffold-generators.test.ts
| Skill | When to Use |
|---|---|
/create-concept | Generate concept specs before implementing handlers |
/implementation-builder | Use SchemaGen for more advanced handler generation |
/concept-validator | Validate concept specs before generating handlers |
/create-view-query | For view-layer concepts, use QueryProgram pipelines instead of ad-hoc filtering/sorting |