Help us improve
Share bugs, ideas, or general feedback.
From oagen
Scaffolds new language emitters for the oagen code generation framework, generating idiomatic SDK code for target languages like Go, Python, Kotlin, or Java.
npx claudepluginhub workos/oagenHow this skill is triggered — by the user, by Claude, or both
Slash command
/oagen:generate-emitterThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Scaffold a complete language emitter for oagen that translates the intermediate representation (IR) into idiomatic SDK code for a target language.
Orchestrates end-to-end SDK generation for a target language, handling both backwards-compatible and fresh scenarios by sequencing sub-skills.
Generates type-safe client SDKs in TypeScript, Python, Go, Java from OpenAPI specs with auth, retries, pagination, and tests.
Initializes new SDK projects from OpenAPI specs using speakeasy quickstart command. Targets TypeScript, Python, Go, Java, C#, PHP, Ruby, Kotlin, Terraform.
Share bugs, ideas, or general feedback.
Scaffold a complete language emitter for oagen that translates the intermediate representation (IR) into idiomatic SDK code for a target language.
oagen has a plugin architecture for code generation. Each target language is an emitter — a TypeScript module that implements the Emitter interface. An emitter receives parsed IR nodes (models, enums, services, etc.) and returns GeneratedFile[] — arrays of { path, content } pairs. The engine orchestrator calls each emitter method, prepends a file header, and writes the results to disk.
Emitters live in external projects (not inside the oagen core repo). They import all oagen types from @workos/oagen and are exported through the emitter project's plugin bundle (e.g., workosEmittersPlugin), which the consumer project's oagen.config.ts imports.
Emitter interface, GeneratedFile shape, overlay integrationApiSpec, TypeRef discriminated union, Model, Enum, Service, OperationDetermine the emitter project path before doing anything:
project argument was provided, use that.AskUserQuestion: "Where is your emitter project? (absolute or relative path, e.g. ../oagen-emitters/node)"All generated files go into this project. Store it as project.
If {project}/package.json does NOT exist, read references/project-scaffold.md and create the boilerplate files listed there. If package.json already exists, skip this.
Before starting, read and understand these oagen core types (all imported from @workos/oagen):
Emitter, EmitterContext, GeneratedFile — the emitter interface contractApiSpec, Model, Enum, Service, Operation, TypeRef — the IR type systemplanOperation, OperationPlan — operation analysis helperstoPascalCase, toSnakeCase, toCamelCase, toKebabCase, toUpperSnakeCase — naming utilitiesAlso read the project's plugin bundle export (e.g., src/plugin.ts) to understand how emitters are registered.
If an sdk_path argument is provided, you MUST thoroughly study that SDK before proceeding to Step 1. The existing SDK's actual code — not generic conventions — drives every design decision.
Before writing any code, establish the exact patterns the emitter will replicate.
sdk_path provided)The existing SDK is the sole source of truth. Study it thoroughly before making any design decisions.
Delegate this to a subagent to keep file-reading noise out of the main context.
Use the Agent tool with subagent_type: Explore and a prompt like:
Explore the SDK at
{sdk_path}. Read at least 10 representative files spanning different concerns. For each of the following pattern categories, find the real code and return: a 1-2 sentence description, a real code snippet, and the source file path.Categories: client architecture (constructor, HTTP methods, resource accessors), model/data types (field types, optionality), request/response types (separate input/output/options types?), serialization (wire/domain conversion?), resource classes (method signatures, parameter patterns, return types), pagination (iterator, page object, etc.), error handling (error hierarchy, status code mapping), testing patterns (framework, mocking, fixtures), entry point/exports (barrel exports, public surface), utilities/common (shared helpers, base classes), file/directory layout (tree structure), constructor/factory (instantiation, config patterns).
Only report patterns actually found — never invent. If a category doesn't apply, say so.
The subagent reads 10+ files and returns only the structured findings — intermediate file reads stay out of context.
Do NOT skip this step. The entire emitter is derived from these findings.
When generating for a live SDK, the overlay provides fileBySymbol — a map from
IR symbol names to the relative file paths where those symbols live in the live SDK.
Emitters can use this to produce GeneratedFile entries with paths that match the
live SDK's layout, enabling the merger to find and merge into the correct files.
Usage in emitter code:
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
return models.map((model) => {
// Check overlay for live SDK path hint
const hintPath = ctx.overlayLookup?.fileBySymbol?.get(model.name);
const filePath = hintPath ?? `models/${toKebabCase(model.name)}.ts`;
return { path: filePath, content: renderModel(model) };
});
}
This is opt-in. Emitters that don't check fileBySymbol generate into their
default layout. The hint is most valuable during /integrate (Phase 3), when
generated files need to merge into the live SDK at the correct locations.
After receiving the subagent's structured summary, present it to the user. For each pattern, include the pattern name, a 1–2 sentence description, a real code snippet from the SDK, and the source file path.
Ask the user to confirm the findings are complete and accurate.
sdk_path)docs/sdk-architecture/{language}.md already exists in the emitter project. If it does, confirm with the user whether changes are needed.Write the full design document to docs/sdk-architecture/{language}.md in the emitter project.
For Scenario A: Derive the doc entirely from the patterns extracted in Step 1a. Every code example must come from the real SDK, not be invented. The design doc is the contract between the study phase and the implementation phase.
For Scenario B: Use the confirmed structural guidelines plus your knowledge of the language ecosystem.
The design doc must include: architecture overview, naming conventions, type mapping table (IR TypeRef to target types), model pattern with example, enum pattern, resource/client pattern, serialization pattern (if applicable), pagination pattern, error handling, retry logic, test pattern, structural guidelines table, directory structure, and utility/common patterns.
CRITICAL for Scenario A: If the existing SDK has patterns NOT covered by the standard emitter scaffold (e.g., serializers, factory functions, dual type systems), those patterns MUST be documented as additional generator files.
Create the following files under src/{language}/ in the emitter project:
{project}/src/{language}/
├── index.ts # Emitter entry point, implements Emitter interface
├── type-map.ts # IR TypeRef → target language type string mapping
├── naming.ts # Target-language naming conventions
├── models.ts # IR Model → target language model/data class
├── enums.ts # IR Enum → target language enum
├── resources.ts # IR Service → target language resource/client class
├── client.ts # HTTP client with retry logic
├── errors.ts # Error class/type hierarchy
├── config.ts # Configuration module/class
├── types-*.ts # Type annotation files (language-specific, may be 0-2 files)
├── tests.ts # Test file generation
├── fixtures.ts # Test fixture generation
└── manifest.ts # Smoke test manifest (operation → SDK method mapping)
Not every language needs every file, and some languages may need additional files beyond this scaffold. For Scenario A, the design doc from Step 1c may identify additional generators needed (e.g., serializers.ts, common.ts, factory.ts, request-types.ts). The file list in the design doc is authoritative, not this generic scaffold.
Omit files that don't apply, and add language-specific utility files as needed. The index.ts must still implement all Emitter interface methods (return [] for inapplicable ones).
The emitter must produce a self-contained, usable SDK. In addition to source files, generateClient (or another appropriate method) must emit these scaffolding files:
src/index.ts) — re-exports all public types, models, enums, exceptions, and the main client class. This is what the extractor and consumers use as the SDK's public surface.tsconfig.json, setup.py, Gemfile) — whatever the target language needs so the generated SDK can be type-checked, built, or imported without manual setup.package.json, *.gemspec, pyproject.toml) — with correct entry points (main, types, exports) so tooling can discover the SDK's public surface.Without these, oagen verify (compat verification) will fail because the extractor cannot find the SDK's entry point. The generated output must be analyzable by the extractor without any manual intervention.
All emitter files import oagen types from @workos/oagen:
import type { Model, TypeRef, Operation } from "@workos/oagen";
import type { EmitterContext, GeneratedFile } from "@workos/oagen";
import { planOperation, toCamelCase, toKebabCase } from "@workos/oagen";
Local imports within the emitter use relative paths:
import { mapTypeRef } from "./type-map.js";
import { className, fileName } from "./naming.js";
Read references/generator-guide.md for detailed instructions on implementing each generator file: type-map.ts, naming.ts, models.ts, enums.ts, resources.ts, serializers.ts (if applicable), client.ts, errors.ts, config.ts, common.ts (if applicable), type signatures, tests.ts, and fixtures.ts.
Before implementing generators, use the Agent tool with subagent_type: Explore to study the reference emitter files you'll be adapting. This keeps ~20K+ tokens of reference source out of the main context.
Read these files from the reference emitter at
{emitterProject}/src/{reference_language}/: type-map.ts, naming.ts, models.ts, enums.ts, resources.ts, client.ts, errors.ts, config.ts, tests.ts, fixtures.tsFor each file, return:
- Purpose: one-line description
- Exports: function/constant signatures
- Pattern: structural flow (setup → transform → output)
- IR inputs: which IR types (Model, Enum, Service, Operation, TypeRef) it consumes
- Output shape: what GeneratedFile paths/content look like
- Language-specific details (do NOT replicate): parts specific to the reference language that must be adapted
Be concise — the consumer needs the pattern, not a line-by-line walkthrough.
Then, for each generator, follow this pattern:
docs/sdk-architecture/{language}.md) for the exact output patterns to produceGeneratedFile[] return type — each function receives IR nodes + EmitterContext and returns file path/content pairsCRITICAL for Scenario A: Each generator must produce output that matches the patterns documented in the design doc. Do NOT invent patterns that weren't found in the existing SDK.
index.ts)Wire everything together by implementing the Emitter interface:
generateTests internally calls generateFixtures — fixtures are not a separate Emitter methodgenerateConfig returns [...generateConfig(ctx), ...generateCommon(ctx)])[] for inapplicable methodsimport type { Emitter } from '@workos/oagen';
export const {language}Emitter: Emitter = {
language: "{language}",
generateModels(models, ctx) { return generateModels(models, ctx); },
generateEnums(enums, ctx) { return generateEnums(enums, ctx); },
generateResources(services, ctx) { return generateResources(services, ctx); },
generateClient(spec, ctx) { return generateClient(spec, ctx); },
generateErrors(ctx) { return generateErrors(ctx); },
generateConfig(ctx) { return generateConfig(ctx); },
generateTypeSignatures(spec, ctx) { return generateTypeSignatures(spec, ctx); },
generateTests(spec, ctx) { return generateTests(spec, ctx); },
buildOperationsMap(spec, ctx) { return buildOperationsMap(spec, ctx); },
fileHeader() { return "{language-appropriate auto-generated file header}"; },
};
Add the emitter to the plugin bundle export (e.g., src/plugin.ts) and re-export from src/index.ts:
// src/plugin.ts
import { {language}Emitter } from './{language}/index.js';
export const workosEmittersPlugin = {
emitters: [/* existing, */ {language}Emitter],
// ...
};
// src/index.ts
export { {language}Emitter } from './{language}/index.js';
The consumer project's oagen.config.ts imports the plugin bundle, so the new emitter is automatically available.
Create test files under test/{language}/ in the emitter project:
{project}/test/{language}/
├── models.test.ts # Model generation tests
├── enums.test.ts # Enum generation tests
├── resources.test.ts # Resource generation tests
├── client.test.ts # Client generation tests
├── errors.test.ts # Error hierarchy tests
└── tests.test.ts # Test generation tests (meta!)
For each generator, test: all type mappings, required vs optional fields, file paths and naming, content snapshots (use toMatchInlineSnapshot() for at least one case per generator), multiple items, and edge cases (nullable, union, nested model refs, enum refs, arrays of models).
For Scenario A: Include at least one "golden file" test per generator that verifies the output matches a known-good excerpt from the existing SDK.
import { describe, it, expect } from "vitest";
import type { EmitterContext, ApiSpec } from "@workos/oagen";
const emptySpec: ApiSpec = {
name: "Test",
version: "1.0.0",
baseUrl: "",
services: [],
models: [],
enums: [],
};
const ctx: EmitterContext = {
namespace: "{snake_case_namespace}",
namespacePascal: "{PascalNamespace}",
spec: emptySpec,
};
# In the emitter project — all tests pass
npx vitest run
# In oagen core — type check and build
npx tsc --noEmit
npx tsup
# Smoke test — generate from an available spec fixture
npx tsx src/cli/index.ts generate \
--spec test/fixtures/{available-spec}.yml \
--lang {language} --output /tmp/test-{language}-sdk --namespace {namespace}
# Determinism — generating twice produces identical output
npx tsx src/cli/index.ts generate \
--spec test/fixtures/{available-spec}.yml \
--lang {language} --output /tmp/test-{language}-sdk-2 --namespace {namespace}
diff -r /tmp/test-{language}-sdk /tmp/test-{language}-sdk-2
# If the target language has a standard linter, run it on the generated output
For Scenario A: Also compare generated output against the existing SDK — structure, patterns, and naming should be recognizably similar.
=== Emitter: {language} ===
Scenario: {A (backwards-compatible) / B (fresh)}
Files created (in {project}):
src/{language}/*.ts — {N} files
test/{language}/*.ts — {N} files
docs/sdk-architecture/{language}.md — SDK design document
Validation:
Tests / Type check / Build / Smoke test / Determinism / Linter / SDK comparison
Patterns replicated: (Scenario A only)
Model / Enum / Resource / Client / Error / Serialization / Pagination / Testing / Barrel exports
Every emitter must implement buildOperationsMap. The smoke test runner uses the operations map (stored in .oagen-manifest.json) to resolve SDK methods from HTTP operations — without it, the smoke test cannot find methods and most operations will be skipped.
The manifest maps every HTTP_METHOD /path to { sdkMethod, service }:
{
"POST /organizations": {
"sdkMethod": "createOrganizations",
"service": "organizations"
},
"GET /organizations/{id}": {
"sdkMethod": "getOrganizations",
"service": "organizations"
}
}
The emitter already knows every operation→method mapping (from resolveMethodName or the equivalent). buildOperationsMap returns this mapping as an OperationsMap object. The orchestrator calls emitter.buildOperationsMap?.(spec, ctx) and writes the result into the operations field of .oagen-manifest.json.
Implementation pattern (manifest.ts):
export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): OperationsMap {
const operations: OperationsMap = {};
for (const service of spec.services) {
const propName = /* service property name on the client */;
for (const op of service.operations) {
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
const methodName = /* resolved method name for this operation */;
operations[httpKey] = { sdkMethod: methodName, service: propName };
}
}
return operations;
}
If the emitter disambiguates operation names (e.g., multiple list operations in one service), the manifest must use the disambiguated names. Run disambiguation before building the manifest, or reuse the same resolved names that generateResources uses.
When ctx.overlayLookup is present (user passed --api-surface), check it before generating default names to preserve backwards compatibility:
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
const existing = ctx.overlayLookup?.methodByOperation.get(httpKey);
if (existing) {
// Use existing.methodName instead of the default generated name
}
Also check interfaceByName and typeAliasByName for type names, and requiredExports for barrel exports. See docs/architecture/emitter-contract.md for the full OverlayLookup field reference.
Read references/common-pitfalls.md before finalizing the emitter, and when debugging failures. Key ones: don't invent patterns (replicate), keep generators pure, namespace everywhere, don't ignore overlay, match the existing test framework exactly.
This skill produces, in the emitter project:
src/{language}/*.ts — emitter generator files implementing the Emitter interfacetest/{language}/*.ts — unit tests for each generatordocs/sdk-architecture/{language}.md — SDK design documentsrc/plugin.ts) and src/index.ts with the new emitter registeredIf the target language has an existing published SDK, scaffold an extractor with /generate-extractor <language>, then run /verify-compat <language> to verify the generated output preserves the existing API surface.