---
title: "Regenerating SDK Code from the OpenAPI Spec"
canonical: "https://test.abhinandan.one/codegen-workflow"
markdown_url: "https://test.abhinandan.one/codegen-workflow.md"
publisher: "Primitive SDKs"
kind: "quickstart"
content_type: "reference"
category: "API Core / OpenAPI Generation"
description: "Run make node-generate python-generate go-generate to regenerate all three SDK clients from openapi/primitive-api.yaml in one pass."
keywords: ["make node-generate python-generate go-generate", "primitive-api.codegen.json", "generate-openapi-artifacts.ts", "fix-generated-api-imports.ts", "generate_api_client.py", "openapi-python-client"]
last_modified: "2026-08-11T18:55:03.476667+00:00"
published_at: "2026-08-11T18:55:03.307499+00:00"
source_files:
  - "packages/api-core/package.json"
  - "packages/api-core/scripts/generate-openapi-artifacts.ts"
  - "packages/api-core/scripts/fix-generated-api-imports.ts"
  - "sdk-python/scripts/generate_api_client.py"
sections:
  - {anchor: "what-gets-regenerated", title: "What gets regenerated"}
  - {anchor: "step-edit-the-openapi-spec", title: "Edit the OpenAPI spec"}
  - {anchor: "step-regenerate-every-sdk-from-the-repo-root", title: "Regenerate every SDK from the repo root"}
  - {anchor: "step-verify-the-regenerated-output", title: "Verify the regenerated output"}
  - {anchor: "step-commit-the-spec-change-and-the-generated-files-together", title: "Commit the spec change and the generated files together"}
  - {anchor: "what-each-generator-does-under-the-hood", title: "What each generator does under the hood"}
  - {anchor: "verify-the-full-pipeline", title: "Verify the full pipeline"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# Regenerating SDK Code from the OpenAPI Spec

Run the root Makefile codegen targets to turn openapi/primitive-api.yaml into typed clients for the Node, Python, and Go SDKs in one pass.

Every typed method in the Node, Python, and Go SDKs is generated from one file: `openapi/primitive-api.yaml`. Change that spec (or `json-schema/email-received-event.schema.json`), regenerate, and all three SDKs pick up the change with matching types.

> **Note:** Prerequisites: a clone of the `sdks` monorepo, Node.js with `pnpm` installed (for the TypeScript codegen scripts), and each SDK's own toolchain (`uv` for Python, Go `>=1.25` for Go) available on `PATH`.

## What gets regenerated

The pipeline runs in two stages, both driven from `openapi/primitive-api.yaml`:

1. **Normalize the spec.** `packages/api-core`'s `generate-openapi-artifacts.ts` reads the hand-written OpenAPI 3.1 YAML and writes `openapi/primitive-api.codegen.json`, a codegen-friendly OpenAPI 3.0.3 JSON document. This is the file every language generator actually consumes. Full detail on this step lives on [OpenAPI Spec Normalization and Codegen Artifacts](https://test.abhinandan.one/openapi-spec-normalization.md).
2. **Generate per-language clients.** Each SDK points its own generator at `primitive-api.codegen.json` and produces a typed client in its own idiom (a `fetch`-based TypeScript client for Node, an `openapi-python-client` package for Python, an `ogen` client for Go). See [Non-TypeScript Codegen: Go and Python Clients](https://test.abhinandan.one/non-typescript-codegen.md) for the Go/Python specifics.

You never hand-edit `primitive-api.codegen.json`. It's a build artifact; edit `openapi/primitive-api.yaml` and regenerate.

### 1. Edit the OpenAPI spec

Make your change in `openapi/primitive-api.yaml`. Author it as OpenAPI 3.1, that's the single source of truth for every downstream client.

```yaml
# openapi/primitive-api.yaml
paths:
  /v1/widgets:
    post:
      operationId: createWidget
      summary: Create a widget
      # ...
```

If the change is to the inbound webhook shape instead, edit `json-schema/email-received-event.schema.json`, see [Webhook Schema Codegen](https://test.abhinandan.one/webhook-schema-codegen.md) for that half of the pipeline.

### 2. Regenerate every SDK from the repo root

Run the root `Makefile` targets. This is the recommended path for any change that must ship in a single commit:

```bash
cd sdks
make node-generate python-generate go-generate
```

Each target wraps that language's native generate script, so it's equivalent to (but easier to keep in sync than) running them individually.

> **Tip:** Iterating on a single language locally before your final commit? Run that language's native script directly instead of all three, for example `pnpm --dir sdk-node generate`. Just make sure the final commit that changes the OpenAPI spec regenerates **all three** languages before you open a PR, reviewers and CI expect the generated output and the spec to move together.

### 3. Verify the regenerated output

Confirm the codegen artifact updated and each SDK's generated client compiles/typechecks:

```bash
git status openapi/primitive-api.codegen.json
make node-check python-check go-check
```

Expect `openapi/primitive-api.codegen.json` to show as modified (or untouched, if your spec edit didn't change the normalized shape), and each `*-check` target to pass. A failing `node-check` after a spec edit usually means a new required field broke an existing hand-written caller, fix the call site, not the generated code.

### 4. Commit the spec change and the generated files together

```bash
git add openapi/primitive-api.yaml openapi/primitive-api.codegen.json \
  packages/api-core/src/openapi/ sdk-node/src/api/ \
  sdk-python/src/primitive/api/ sdk-go/api/
git commit -m "Add POST /v1/widgets and regenerate SDK clients"
```

Commit the spec edit and every regenerated file in the same commit (or the same PR) so the repo never has an SDK whose generated code disagrees with the spec it was built from.

## What each generator does under the hood

Each language runs its own generator against `openapi/primitive-api.codegen.json`, then applies language-specific fixups to the output.

**Choose one of the following:**

**Node / TypeScript**

`packages/api-core` owns the TypeScript generation. Its `generate` script runs both stages:

```json
// packages/api-core/package.json
{
  "scripts": {
    "generate:openapi": "tsx scripts/generate-openapi-artifacts.ts",
    "generate:api": "openapi-ts -f openapi-ts.config.ts && tsx scripts/fix-generated-api-imports.ts",
    "generate": "pnpm generate:openapi && pnpm generate:api"
  }
}
```

`generate-openapi-artifacts.ts` reads `openapi/primitive-api.yaml`, writes the normalized `primitive-api.codegen.json`, and also emits two TypeScript files consumed elsewhere in the repo: `src/openapi/openapi.generated.ts` (the raw OpenAPI document as a JS constant, for tooling like `primitive describe`) and `src/openapi/operations.generated.ts` (the [operation manifest](https://test.abhinandan.one/operation-manifest.md), one entry per operation with its command name, method, path, parameters, and inlined request/response JSON Schemas).

`generate:api` then runs `@hey-api/openapi-ts` against the normalized spec to produce the actual fetch client, followed by `fix-generated-api-imports.ts`, a post-processing pass that:

- adds `.js` extensions to every relative import (required for the package's ESM output)
- guards the `Content-Type: application/json` header on optional-body operations so it's only sent when a body is actually present
- repairs the generated `MemoryJsonValue` type, which `@hey-api/openapi-ts` currently widens to `unknown` on the OpenAPI 3.1 `type: "null"` branch

Full detail on that fixup pass lives on [Generated TypeScript Client Fixups](https://test.abhinandan.one/typescript-client-fixups.md). Both `sdk-node` and `cli-node` bundle `@primitivedotdev/api-core`'s output inline; it is never published as its own package. See [What is API Core?](https://test.abhinandan.one/api-core-overview.md).

**Python**

`sdk-python/scripts/generate_api_client.py` drives `openapi-python-client` against the same `primitive-api.codegen.json`:

```python
# sdk-python/scripts/generate_api_client.py
subprocess.run(
    [
        "openapi-python-client", "generate",
        "--meta", "none",
        "--config", str(CONFIG_PATH),
        "--path", str(SPEC_PATH),   # openapi/primitive-api.codegen.json
        "--output-path", str(output_path),
    ],
    check=True,
    cwd=SDK_ROOT,
)
```

It generates into a temp directory, then copies `api/`, `client.py`, `errors.py`, `models/`, and `types.py` into `sdk-python/src/primitive/api/`, replacing whatever was there.

Three post-processing passes run afterward:

- `dedupe_imports` removes duplicate module-level `from X import ...` lines that `openapi-python-client` 0.28.3 occasionally emits.
- `guard_optional_body_content_type` re-indents the `Content-Type` header assignment so it only fires when an optional body is actually sent.
- `use_bytes_for_file_responses` fixes binary-response handling to read `response.content` instead of `response.text`.

Run it directly with:

```bash
cd sdk-python
uv sync --dev
uv run python scripts/generate_schema_module.py
uv run python scripts/generate_models.py
uv run python scripts/generate_api_client.py
```

Or via the root Makefile target, which wraps all three scripts: `make python-generate`.

**Go**

The Go SDK generates its `sdk-go/api` package with `ogen` from the normalized codegen spec. Run it with:

```bash
make go-generate
```

See [Non-TypeScript Codegen: Go and Python Clients](https://test.abhinandan.one/non-typescript-codegen.md) for the Go-specific generator config and fixups.

## Verify the full pipeline

Verify a regeneration by running each language's check target (typecheck, lint, and build, not just tests) before you commit:

```bash
make node-check
make python-check
make go-check
```

Or run everything the CI runs in one shot:

```bash
make check
```

> **Warning:** Never hand-edit anything under `packages/api-core/src/openapi/`, `sdk-node/src/api/`, `sdk-python/src/primitive/api/`, or `sdk-go/api/`. Every file in those trees is regenerated on the next `make *-generate` run and any manual edit is silently overwritten. If the generated output is wrong, fix the generator script or the OpenAPI spec, not the output.
