# sdkgen

> Turns the model into SDKs. The generator. It reads the model apidef produced and writes one idiomatic SDK per target language, each with the same operation pipeline, the same feature set, generated documentation and an offline test suite. Part of the Voxgig SDK toolchain. Checked 8 September 2026.

## What it is

sdkgen is the second half of the toolchain. It does not read your OpenAPI spec: apidef does that, and hands sdkgen a model of entities, operations, fields and flows. sdkgen turns that model into code.

The unit of output is a `target`, and 23 of them are language SDKs. Twenty-two are bundled with the generator: TypeScript, JavaScript, Go, Python, PHP, Ruby, Lua, C#, Java, Kotlin, Scala, Swift, Dart, Rust, C, C++, Zig, Perl, Clojure, Elixir, OCaml and Lean. The twenty-third, Haskell, arrives as a package, `@voxgig/sdkgen-haskell`, which is how further targets arrive rather than as changes to the generator.

Four further bundled targets are consumer targets: they wrap a language SDK rather than call the API themselves. `go-cli` and `go-mcp` wrap the Go SDK to produce a command-line tool and an MCP server. `py-data` wraps the Python SDK for analysts. `seneca-provider` wraps the TypeScript SDK as a Seneca plugin. This is why one spec yields a CLI and an agent-callable server without anyone writing either.

Every target is generated from the same model through the same pipeline, so behaviour does not fork by language. A retry policy means the same thing in Rust as in Ruby because both were generated from one description of it, not written twice.

The generated code is yours, and regeneration is not a bulldozer: when a generated file already exists, the new content is three-way diff-merged into it, so the generator's updates and local reality usually converge without anyone resolving anything. A project that prefers a clean slate switches to plain overwrite with one line of config, which is what both demo repositories do. Either way a bug is fixed in the model, a template or a component rather than in the output, because a resync of a vendored target reverts hand edits and the merge is a convenience rather than a guarantee. The one file the toolchain will not touch is `.sdk/model/project.aon`, which is created once and is yours to edit.

## Facts

- Package: @voxgig/sdkgen (https://www.npmjs.com/package/@voxgig/sdkgen)
- Source: voxgig/sdkgen (https://github.com/voxgig/sdkgen)
- Licence: MIT
- Runtime: Node.js 24 or later
- Language targets: 23, of which 22 are bundled
- Features: 19, all off until switched on

## Concepts

### Targets, features and kinds

A `target` is something to generate: a language, or a consumer of a language. A `feature` is optional behaviour a generated SDK can carry, like `retry` or `cache`. Both are `kinds`, and the CLI's verbs are built from the kind registry, so `docs` is a kind too and `voxgig-sdkgen docs add` works without any dispatch code written for it. That is the seam a package like `@voxgig/docgen` plugs into.

### Features are off until you switch them on

All 19 features are inactive by default. An SDK you have not configured behaves as if none of them existed: no retries, no cache, no logging, no measurable overhead. You activate one by name in the client options, and override its defaults there. Three are core (`test`, `log`, `secrets`), fifteen are enterprise capabilities (`retry`, `timeout`, `ratelimit`, `cache`, `cost`, `idempotency`, `paging`, `streaming`, `proxy`, `telemetry`, `metrics`, `debug`, `audit`, `clienttrack`, `rbac`), and one supports testing (`netsim`, which simulates network conditions).

### Order matters for the features that wrap the transport

`retry`, `secrets` and `timeout` each wrap whatever transport is already installed, so the order you activate them in is the nesting order. A feature activated later wraps one activated earlier. This is why they are activated as an ordered list rather than an unordered map: a timeout inside a retry and a retry inside a timeout are different policies, and the model has to be able to say which you meant.

### The CLI scaffolds, it does not generate

`voxgig-sdkgen` copies targets and features into a project's `.sdk/` directory and registers them. It does not run code generation. Generation is driven by `@voxgig/model` through the programmatic API, which in a scaffolded project is `npm run generate`. Expect to lose time to this if you assume the CLI is the generator.

### Entities, not endpoints

The generated surface is a small set of capitalised entities with the operations they support, not a method per URL. `client.Moon().list({ planet_id })` rather than a path and a query string assembled by hand. The mental model stays the size of the API's data, not the size of its routing table, which matters for people and matters more for agents.

### Every SDK ships an offline test mode

The `test` feature swaps the HTTP transport for an in-memory mock, so a generated SDK's unit tests run with no server, no network and no credentials. The mock starts empty and you seed it with the records the test needs. Every generated target also carries a `readme_examples` test that extracts each code block from its own README, compiles it and runs the runnable ones, so a documented example that does not work fails that target's build.

## Examples

### Add the targets and features you want

Run from the directory holding `.sdk/`. Names are comma-separated, so several arrive at once. `target add` copies the target model, its generator components and its templates, and registers it in the index; it also pulls in the `test` feature, because every target's generated suite depends on it. `-y` plans the work and writes nothing.

`in <project>/.sdk`

```bash
voxgig-sdkgen target add ts,py,go
voxgig-sdkgen feature add retry,timeout,secrets
voxgig-sdkgen -y target add rust        # dry run: log the plan, write nothing

npm run generate                        # this is what generates; the CLI does not
```

A target reference can also name a package, so `voxgig-sdkgen target add @voxgig/sdkgen-haskell/haskell` adds a language the generator does not bundle.

### Call the API as entities (from solardemo)

solardemo has two entities, and `Moon` is nested under `Planet`, so a moon call carries the `planet_id` it is nested by. Operations resolve to entities rather than raw records; `.data()` gets you the record inside.

`solardemo, TypeScript`

```typescript
import { SolardemoSDK } from '@voxgig-sdk/solardemo'

const client = new SolardemoSDK()

// list() resolves to an array of Moon entities
const moons = await client.Moon().list({ planet_id: 'example' })

// load() returns the entity and throws on failure
const moon = await client.Moon().load({
  planet_id: 'example_planet_id',
  id: 'example_id',
})

// create() returns the created entity; the id comes off its data()
const created = await client.Moon().create({
  planet_id: 'example_planet_id',
  diameter: 1,
  id: 'example_id',
  kind: 'example_kind',
  name: 'example_name',
})

await client.Moon().remove({
  id: created.data().id!,
  planet_id: 'example_planet_id',
})
```

### Configure a client that needs a server variable and features (from elementdemo)

elementdemo's base URL is templated on an `account_id`, so `server` is required rather than optional. Features are named in the client options and are inactive until they appear there. Every option below has a default; naming one overrides only that one.

`elementdemo, TypeScript`

```typescript
import { ElementdemoSDK } from '@voxgig-sdk/elementdemo'

const client = new ElementdemoSDK({
  apikey: process.env.ELEMENTDEMO_APIKEY,

  // Required: this API's server URL is a template over these.
  server: { account_id: '<account_id>' },

  feature: {
    // Defaults: 2 retries, 50ms to 2000ms, factor 2,
    // on 408, 425, 429, 500, 502, 503 and 504.
    retry: { active: true, retries: 3 },

    // Default 30000ms. Wraps the transport, so it wraps retry above it.
    timeout: { active: true, ms: 5000 },
  },
})

const elements = await client.Element().list()
```

`retry` and `timeout` both wrap the transport, so the order they are activated in decides which one sees the other's attempts.

### The same SDK, in Python (from elementdemo)

The model is the same, so the surface is the same; what changes is what each language calls idiomatic. Python returns records as dicts and raises on error, where TypeScript resolves entities and throws.

`elementdemo, Python`

```python
import os
from elementdemo_sdk import ElementdemoSDK

client = ElementdemoSDK({
    "apikey": os.environ.get("ELEMENTDEMO_APIKEY"),
    "server": {
        "account_id": "<account_id>",
    },
})

for element in client.Element().list():
    print(element)
```

### Test with no server and no credentials (from solardemo)

`test` swaps the transport for an in-memory mock. It starts empty, so seed it with the records the test needs, keyed by entity name and then by id. This is what makes a generated SDK's own suite runnable in CI without a sandbox account.

`solardemo, offline test in TypeScript`

```typescript
// Shape: { entity: { <entity-name>: { <id>: <record> } } }
const client = SolardemoSDK.test({
  entity: {
    moon: {
      test01: { id: 'test01', planet_id: 'example_planet_id', diameter: 1 },
    },
  },
})

const moon = await client.Moon().load({
  planet_id: 'example_planet_id',
  id: 'test01',
})
```

Pair it with `netsim` to make the mock fail on purpose: `{ netsim: { active: true, failTimes: 2, failStatus: 503 } }` proves your retry policy rather than assuming it.

### Write a feature the generator does not ship (from elementdemo)

elementdemo carries a feature of its own, `elementcard`, which renders an element-shaped result as an ASCII periodic-table tile. It lives in the project's own sdkgen package under `ext/`, and it is declared in the model exactly the way a bundled feature is. It is shape-triggered rather than bound to an entity: any single-record result carrying `number`, `symbol`, `name` and `mass` gets a card.

`.sdk/model/feature/elementcard.aon`

```aontu
main: kit: feature: elementcard: {

  name: key()
  title: "ASCII periodic-table tile for element-shaped results"
  version: '0.1.0'
  active: true
  base: '../ext/.sdk'
  package: '@voxgig-sdk/sdkgen-elementdemo-ext'
  transport: 'none'

  config: {
    # Off by default, like every optional feature.
    options: {
      active: false
      print: false
    }
  }

  hook: {
    PreResult: { active: true }
  }
}
```

`transport: 'none'` says this one does not wrap the transport, so it carries none of the ordering constraints `retry` and `timeout` do. The `ext/` package supplies an implementation per language, which is why the same card renders from TypeScript, Python, Go, Java and Bash.

## Reference

### CLI actions

| | |
| --- | --- |
| `target add <ref>[,<ref>...]` | Scaffold language or consumer targets into `.sdk/`, and register them. |
| `feature add <name>[,<name>...]` | Scaffold features into `.sdk/`. |
| `docs add <name>` | Scaffold a documentation target, from a package that supplies the `docs` kind. |
| `package add <pkg>` | Install everything an sdkgen package provides. `--only <kind>:<name>` narrows it, `--alias <name>=<alias>` renames. |
| `package update <pkg>` | Refresh an installed package. `--force` overwrites locally changed files and lists what it discarded; `--no-fetch` uses the copy already installed. |
| `package check <path>` | Validate a package. The one action that runs where there is no project. |
| `doctor` | Report on the project. A non-zero exit is a finding to act on. |

### CLI options

| | |
| --- | --- |
| `--debug, -g <level>` | `trace`, `debug`, `info` (default), `warn`, `error` or `fatal`. |
| `--dryrun, -y` | Plan the work and log it. Write no files. |
| `--help, -h / --version, -v` | Print usage or version, then exit. |

### The 19 features

| | |
| --- | --- |
| `Core` | `test` (in-memory mock transport), `log` (structured logging), `secrets` (resolve the credential through a provider chain, and exchange a refresh token for short-lived access tokens) |
| `Resilience` | `retry`, `timeout`, `ratelimit`, `cache` |
| `Correct writes` | `idempotency` |
| `Large result sets` | `paging`, `streaming` |
| `Observability` | `telemetry`, `metrics`, `audit`, `debug`, `clienttrack` |
| `Governance and cost` | `rbac`, `cost`, `proxy` |
| `Test support` | `netsim`, which simulates network conditions so a resilience policy can be proved |

### Where things live in a project

| | |
| --- | --- |
| `.sdk/model/project.aon` | Yours. Created once, never overwritten. |
| `.sdk/model/entity/*.aon` | The entities: fields, operations, id, relations. Toolchain-derived and refreshed. |
| `.sdk/model/target/*.aon` | One per target, plus `target-index.aon` registering them. |
| `.sdk/model/feature/*.aon` | One per feature, plus `feature-index.aon`. |
| `.sdk/model/flow/*.aon` | The generated flows: ordered operation sequences with assertions. |
| `.sdk/src/cmp/<target>/` | The generator components for a target. |
| `.sdk/tm/<target>/` | The templates for a target. |
| `ts/, py/, go/, ...` | Generated output. Three-way merged on each regenerate by default, overwritten outright in a project configured for it. |

## First-party documentation

- [sdkgen documentation index](https://github.com/voxgig/sdkgen/tree/main/docs)
- [Tutorial: generate your first SDK](https://github.com/voxgig/sdkgen/blob/main/docs/tutorial.md)
- [Reference: the feature catalogue](https://github.com/voxgig/sdkgen/blob/main/docs/reference/features.md)
- [Reference: the voxgig-sdkgen CLI](https://github.com/voxgig/sdkgen/blob/main/docs/reference/cli.md)
- [Reference: the model schema](https://github.com/voxgig/sdkgen/blob/main/docs/reference/model.md)
- [Reference: project layout](https://github.com/voxgig/sdkgen/blob/main/docs/reference/project-layout.md)
- [Explanation: architecture](https://github.com/voxgig/sdkgen/blob/main/docs/explanation/architecture.md)
- [How-to: author a new language target](https://github.com/voxgig/sdkgen/blob/main/docs/how-to/author-a-new-language.md)
- [AGENTS.md: the guide for agents modifying the generator](https://github.com/voxgig/sdkgen/blob/main/AGENTS.md)

## The rest of the toolchain

- [The toolchain documentation index](https://voxgig.com/sdk/docs): the pipeline, the components, and the two worked examples.
- [apidef](https://voxgig.com/sdk/docs/apidef): Turns a spec into a model.
- [create-sdkgen](https://voxgig.com/sdk/docs/create-sdkgen): Scaffolds a project.
- [docgen](https://voxgig.com/sdk/docs/docgen): Generates documentation targets.
- apigen: Not yet published.
- [Voxgig SDK Generator](https://voxgig.com/sdk)
