> For the complete documentation index, see [llms.txt](/llms.txt)

# Migrate from Speakeasy

Speakeasy open-sourced its generator in September 2026 and repositioned around AI governance. You
keep the SDKs you already generated — but the company that maintained the pipeline behind them sells
something else now. Glotto picks it up, and adds the part that is hard to rebuild in-house: every
regeneration is **verified, not just emitted** — compile and contract statuses, per-target drift, and
custom-code preservation, recorded in a [verification report](/docs/verification-report) delivered to
CI, the release PR, and the console.

**You can run this conversion yourself.** `glotto migrate speakeasy` ships in the published CLI —
unlike [migrating off Fern](/docs/migrate-from-fern), where the conversion has to read your API
surface through the spec pipeline and stays on our side. Everything it needs is already in your git
repo: Speakeasy keeps its config in your tree, not in a dashboard, which is the same practical
advantage Fern has and [Stainless](/docs/migrate-from-stainless) does not.

## What Glotto needs from you

Two files, at two levels — and the second level is the one that gets forgotten.

**The root `.speakeasy/workflow.yaml`** says what to generate and from where:

```yaml
sources:
    Acme Pets API (Public):
        inputs:
            - location: ./openapi/petstore.yaml
        overlays:
            - location: ./overlays/names.yaml
        registry:
            location: registry.speakeasyapi.dev/acme/acme/petstore-public
    Acme Pets API (Internal):
        inputs:
            - location: registry.speakeasyapi.dev/acme/acme/petstore-internal
```

**A `.speakeasy/gen.yaml` beside each generated SDK** says what that SDK is *called*:

```yaml
python:
    version: 3.4.1
    packageName: acme-petstore
    moduleName: acmepets
```

Your package name and module namespace live **only** in that second file. The conversion finds them
by walking each target's `output` path, so it reads the tree rather than the one file you point it
at. If a target's `gen.yaml` is missing or unreadable, you get
`GLOTTO_MIGRATE_SPEAKEASY_GEN_MISSING` naming the exact path it looked for — the target is still
migrated from the workflow alone, but its package identity is not, and a partial migration never
passes itself off as a complete one.

## How you get it

```sh
npm install -g @glotto/cli
glotto login
glotto migrate speakeasy
```

The defaults are the layout you already have: `.speakeasy/workflow.yaml` in, `glotto.yml` out.
`--in` and `--out` move either end, `--force` overwrites an existing config, and `--report` also
writes the report to a file.

### If your workflow declares more than one source

It probably does — most real ones do. `glotto.yml` takes exactly **one** input document, so the
conversion stops and names them:

```
GLOTTO_MIGRATE_SPEAKEASY_SOURCE_AMBIGUOUS
```

**Nothing is written.** This is the one place the tool refuses rather than choosing, and it refuses
on purpose: picking a source silently would regenerate your SDKs from a document you did not select,
and nothing would surface it until one of your consumers broke. Re-run naming the one you want —
quoted, because real source ids contain spaces and parentheses:

```sh
glotto migrate speakeasy --source 'Acme Pets API (Public)'
```

The message tells you which of the two real shapes you have. Several genuinely different documents
(an Azure spec and a GCP spec) are several Glotto projects — migrate each separately. One document
under per-language overlay stacks is one API, and you pick the stack you want as the baseline.

### If a source is backed by the Speakeasy registry

A `registry.speakeasyapi.dev/...` input resolves against the service you are leaving, so carrying it
across would hand you a config that validates and then 404s in **your** CI. You get
`GLOTTO_MIGRATE_SPEAKEASY_DEAD_REGISTRY` instead.

Unlike the multi-source case above, this one does **not** stop — the rest of your workflow still
converts, and the conversion writes a `glotto.yml` with `openapi.source: REPLACE_ME` where the dead
reference would have gone. Export the document to a local path, fill that in, and you are done.

## What you get back

A `glotto.yml`, and a report that gives **every key you wrote** exactly one of four verdicts. The
four are the whole point, and the third and fourth are where the value is:

| Verdict | What it means |
|---|---|
| **Mapped** | Translated. The line names the `glotto.yml` key it became. |
| **Already guaranteed** | You are not losing this — Glotto provides it unconditionally, so it needs no configuration. `enableCustomCodeRegions` becomes a [three-way merge](/docs/custom-code) on every regeneration; `forwardCompatibleEnumsByDefault` is simply how Glotto emits enums. |
| **Not honored** | An emission-shape knob Glotto decides itself (`maintainOpenAPIOrder`, `responseFormat`, `multipartArrayFormat`), or vendor bookkeeping (`management`, `features`) that records generation checksums rather than anything you chose. Most of a real per-language block is this. |
| **Unmapped** | Genuinely no `glotto.yml` home, reported at its precise dotted path. **This is the list that needs your attention**, and it is short because the other three tiers are not in it. |

A migration report that called a guarantee a loss would read as a list of things you are giving up,
when it is the list of things you no longer have to configure. Every code has a row on the
[migration diagnostics reference](/docs/diagnostics-migrations).

### Your publish credentials are dropped, loudly

Your workflow carries registry tokens. A credential is **never** written into your `glotto.yml` —
not the value, not the key. The **intent** carries over — `publish.pypi.token` becomes
`publish: { pypi: true }`, so your release pipeline can be rebuilt — and the credential itself is
never written into a file you commit. You get `GLOTTO_MIGRATE_SPEAKEASY_SECRET_DROPPED` naming each
one, because a token dropped silently is a release pipeline that fails later for no stated reason.
Configure them as repository secrets for the release workflow instead.

### Targets that are not SDK languages

Speakeasy lets a target be something other than an SDK language, and the three cases land in two
different places.

`terraform` and `cli` become ordinary `targets` entries — Glotto generates a
[Terraform provider](/docs/terraform) and a [CLI](/docs/generated-cli) as targets, under those same
names. `mcp-typescript` does not: Glotto's MCP server is its own top-level `mcp` block rather than a
language target, so the conversion routes it there. Mapping that one by its Speakeasy name would put
`mcp-typescript` under `targets`, where it is not a valid language, and you would find out at
validation rather than here.

## What you still fill in by hand

A Speakeasy workflow carries no company identity, so the conversion cannot invent one. The report
ends with a **placeholder count** — two, on a typical project: your organization name and a contact
address. Each is written as an obvious stand-in (`REPLACE_ME`, an `example.invalid` address) rather
than a plausible guess, and each is listed in the report under *Placeholders (needs review)* rather
than left for you to discover.

Your **API surface is not one of them.** Your workflow points at an OpenAPI document, so Glotto reads
it and writes the `resources` block for you — operations grouped by their first tag, each keyed by
its `operationId`:

```yaml
resources:
  pets:
    methods:
      createPet: post /pets
      getPet: get /pets/{petId}
      listPets: get /pets
```

That key is the method name Glotto emits, taken from the same `operationId` your Speakeasy SDK
derived its own name from — so for any operation your spec gives an `operationId`, the migration does
not rename your surface on its own initiative. Renaming is available and deliberate, never something
done to you.

An operation with **no** `operationId` has no such name to carry, so Glotto builds one from the
route (`get /pets/{petId}` becomes `get_pets_petId_`) — and **says so in the report**, one line per
operation, because that is a name we chose and yours may differ.

**It is not a guarantee that every member name matches, and the report is where you check.** Your
`gen.yaml` may carry dated `fixes` flags and an `sdkClassName`, each of which moved names in the SDK
you have today; the conversion reports every one it finds, under exactly that heading, because those
are the reasons a migrated SDK's public members could differ from the one it replaces. Pinning them
is a separate step, not part of this conversion.

If the document cannot be read, you get a clearly marked placeholder resource instead and the report
says which case you hit — a `registry.speakeasyapi.dev` reference, a URL (this command reads local
files and will not fetch one), a path that does not resolve, a document that will not parse, or one
that declares no operations. That one you do have to replace before you generate.

**Overlays and merged documents are applied before the surface is read.** Your workflow may stack
overlays or merge further documents on top of the document your source names, and an OpenAPI
Overlay can add or remove an operation. The conversion applies them first, in order and with the
same functions Glotto runs when it generates, so the `resources` block describes the surface your
SDK is generated from: an operation an overlay removes is not declared, and one an overlay adds
keeps its own name. The report says so under `resources`.

When a transform cannot be applied locally, you get a warning naming it and the reason, and the
block describes the primary document instead. That happens when an overlay is a URL (this command
reads local files and will not fetch one), a file is missing or will not parse, or the document is
Swagger 2.0 (Glotto converts it before applying anything). It also happens when an overlay action
matches nothing in your document. The Overlay standard skips such an action, but Glotto refuses it,
so fix or remove it before you generate. It also happens when an action's JSONPath filter may read a
server URL or an example. Glotto normalizes those values before it applies overlays, so this command
cannot tell what the filter will select. Any other correction Glotto refuses, such as an invalid
JSONPath or a merged document that changes nothing, is reported the same way, with its reason. When
that verdict depends on a server URL or an example, the warning says it cannot be judged locally
instead of predicting what `glotto generate` will do.

A transform that does apply can still get a separate warning. That happens when an overlay action
or a merge anchor reaches an example, even through a wildcard, or a merged document writes a server
list or an example, or is anchored on a server. Your `resources`
block includes its effect, but `glotto generate` compares those normalized values and may refuse
it, so run it to confirm.

## Next steps

Fill in the placeholders the report names, then:

```sh
glotto generate
```

From there every regeneration is checked rather than trusted: [drift detection](/docs/drift),
compile and contract verification, and a [verification report](/docs/verification-report) on each
one. That is the part that was never really the generator's job, and it is why the second push — and
every push after it — is the thing Glotto is actually for.
