Skip to content
42 changes: 39 additions & 3 deletions fern/products/sdks/custom-code.mdx
Original file line number Diff line number Diff line change
@@ -1,19 +1,55 @@
---
title: Adding custom code
headline: Adding custom code (overview)
description: Extend Fern-generated SDKs with custom methods, logic, and dependencies. Use .fernignore to protect your code from being overwritten during regeneration.
description: Extend Fern-generated SDKs with custom methods, logic, and dependencies. Use Fern Replay for line-level edits and .fernignore for files you own end-to-end.
---


Fern-generated SDKs are designed to be extended with custom logic, methods, and dependencies. If you want your SDK to do more than just make basic API calls (like combining multiple calls, processing data, adding utilities), you can add custom code that lives in harmony with the generated code.

You can also add custom methods by inheriting the Fern generated client and extending it, plus add any dependencies that your custom methods depend on in your `generators.yml` file.

Fern provides two complementary tools for keeping your customizations safe across regenerations: **Replay** for line-level edits to generated files, and **`.fernignore`** for files you own end-to-end.

## Preserving customizations with replay

<Note>
Replay requires the latest Fern CLI and SDK generator versions. Run `fern upgrade` to make sure you're current.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] reported by reviewdog 🐶
[FernStyles.Current] Avoid time-relative terms like 'latest' that become outdated

</Note>

Replay automatically preserves the edits you make to your generated SDK across regenerations. When you commit a change to generated code, Replay records it as a patch. On the next `fern generate`, the patch is reapplied to the freshly generated SDK using a 3-way merge.

Replay handles the common case where `.fernignore` is too heavy-handed: you want to change a few lines, not own the whole file forever.

### How it works

1. Edit generated SDK code and commit to your SDK repo.
2. On the next `fern generate`, Replay scans your repo's history for customer commits since the last `[fern-generated]` commit and stores each one as a tracked patch in `.fern/replay.lock`.
3. The patches are reapplied on top of the freshly generated code via 3-way merge.
4. The result lands as a `[fern-replay]` commit on top of `[fern-generated]` in the regeneration PR.

```text title="Regeneration PR"
* abc123 (HEAD -> main) [fern-replay] Apply customizations
* 789abc [fern-generated] Update SDK to spec rev 0451
* 234bcd Add helpers on top of User type
* ...
```

If the generator and your customization changed the same lines, Replay reports the conflict in the PR body. Run `fern replay resolve` locally to walk through it.

### Requirements

Replay requires `pull-request` output mode. If your SDK uses `release` or `push` mode, see the [migration guide](/learn/sdks/overview/replay-migration) for the switch.

### Disable replay

To disable Replay for a specific generator, set `replay.enabled: false` in `generators.yml`. See the [`replay` reference](/learn/sdks/reference/generators-yml#replay) for global and per-generator configuration.

## Using `.fernignore` to preserve your customizations

Once you add files containing custom code, use `.fernignore` to protect your custom code from being overwritten when Fern regenerates your SDK.
Use `.fernignore` to protect files you own end-to-end from being overwritten during SDK regeneration. Where Replay reapplies line-level edits to generated files, `.fernignore` makes the file entirely yours. The generator won't touch it, but generator updates to that file also stop flowing.

Simply add your custom files to the SDK repository and list them in `.fernignore`. Fern won't override any files listed there. A `.fernignore` file is automatically created in your SDK repository when you use GitHub publishing.
Add your custom files to the SDK repository and list them in `.fernignore`. A `.fernignore` file is automatically created in your SDK repository when you use GitHub publishing.

<Note>`.fernignore` applies only to SDK generation. It has no effect on [Fern Docs](/learn/docs/getting-started/overview) builds.</Note>

Expand Down
31 changes: 31 additions & 0 deletions fern/products/sdks/reference/generators-yml-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,19 @@ autorelease: true
Set to `true` to enable Autorelease, `false` to disable it. [Per-generator settings](#autorelease-2) override this global configuration.
</ParamField>

## `replay`

Override [Fern Replay](/learn/sdks/overview/custom-code#preserving-customizations-with-replay) behavior globally. Replay runs based on your organization's configuration; use this setting to disable it across all generators in this `generators.yml`. Alternatively, you can [configure Replay for individual SDKs](#replay-2).

```yaml title="generators.yml"
replay:
enabled: false
```

<ParamField path="replay.enabled" type="boolean" required={false} toc={true}>
Set to `false` to skip Replay for all SDK generations, even if Replay is enabled for your organization. [Per-generator settings](#replay-2) override this global configuration.
</ParamField>

## `aliases`

Define shortcuts that map to multiple generator groups, allowing you to run several groups with a single command. When you run `fern generate --group <alias>`, all groups in the alias run in parallel. You can also set an alias as your `default-group`.
Expand Down Expand Up @@ -1019,6 +1032,24 @@ groups:
Set to `true` to enable Autorelease for this generator, `false` to disable it. Per-generator settings override the global [`autorelease`](#autorelease-1) configuration.
</ParamField>

#### `replay`

Override [Fern Replay](/learn/sdks/overview/custom-code#preserving-customizations-with-replay) for an individual SDK. Alternatively, you can [configure Replay globally for all SDKs](#replay-1).

```yml title="generators.yml" {6-7}
groups:
ts-sdk:
generators:
- name: fernapi/fern-typescript-sdk
...
replay:
enabled: false
```

<ParamField path="replay.enabled" type="boolean" required={false} toc={true}>
Set to `false` to skip Replay for this generator, even if Replay is enabled for your organization or globally in this `generators.yml`. Per-generator settings override the global [`replay`](#replay-1) configuration.
</ParamField>

#### `snippets`

Configures snippets for a particular generator.
Expand Down
167 changes: 167 additions & 0 deletions fern/products/sdks/replay-migration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
title: Migrating to Replay
headline: Migrating from release or push mode to pull-request mode
description: Step-by-step guide for switching SDK generation from release or push mode to pull-request mode so Fern Replay can run.
---

<Note>
Replay requires the latest Fern CLI and SDK generator versions. Run `fern upgrade` to make sure you're current.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] reported by reviewdog 🐶
[FernStyles.Current] Avoid time-relative terms like 'latest' that become outdated

</Note>

[Fern Replay](/learn/sdks/overview/custom-code#preserving-customizations-with-replay) requires `pull-request` output mode. This guide covers the migration from `release` or `push` mode in five phases.

## What changes

- SDK updates arrive as PRs instead of direct pushes to `main`.
- Customizations to generated code survive regeneration via 3-way merge.
- Version bumps are computed from pure generator output, never contaminated by customizations.
- Publishing is decoupled from generation, so you control when to release.

## Before you start

- Install the [Fern GitHub App](https://github.com/apps/fern-api) on your SDK repositories.
- Update to the latest Fern CLI and SDK generator versions (`fern upgrade`).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] reported by reviewdog 🐶
[FernStyles.Current] Avoid time-relative terms like 'latest' that become outdated


## Phase 1: Move publishing secrets to the SDK repo

Fern sets GitHub Actions secrets in your SDK repo on every generation. When you switch to PR mode, that write would clobber any secrets you set directly. To avoid this, move secret ownership from the config repo to the SDK repo before flipping modes.

For each SDK repo:

1. Identify which secrets your `generators.yml` references for publishing:
- Python: `PYPI_USERNAME`, `PYPI_PASSWORD`
- TypeScript / npm: `NPM_TOKEN`
- Java / Maven: `MAVEN_USERNAME`, `MAVEN_PASSWORD`, `MAVEN_SIGNATURE_KID`, `MAVEN_SIGNATURE_PASSWORD`, `MAVEN_SIGNATURE_SECRET_KEY`
- Ruby: `RUBY_GEMS_API_KEY`
- C# / NuGet: `NUGET_API_KEY`
- Rust / Crates: `CRATES_TOKEN`
2. Add the real publish credentials directly to the SDK repo: **Settings → Secrets and variables → Actions → New repository secret**.
3. Remove the publish credential values from the config repo's `generators.yml`:

```yaml title="generators.yml"
# BEFORE
groups:
python-sdk:
generators:
- name: fernapi/fern-python-sdk
version: "..."
github:
repository: customer/python-sdk
mode: pull-request
output:
pypi:
username: $PYPI_USERNAME
password: $PYPI_PASSWORD

# AFTER (remove the output/pypi block entirely)
groups:
python-sdk:
generators:
- name: fernapi/fern-python-sdk
version: "..."
github:
repository: customer/python-sdk
mode: pull-request
```

4. Run a generation. Confirm the SDK repo's secrets weren't overwritten by checking the "Last updated" timestamp under **SDK repo → Settings → Secrets**.

## Phase 2: Switch output mode

In `generators.yml`:

```yaml title="generators.yml" {3}
github:
repository: customer/python-sdk
mode: pull-request # was: mode: release (or: mode: push)
```

If you use `version: AUTO`, no other changes are needed. Autoversioning runs as part of the generator-cli pipeline and diffs pure generator output, so customer customizations never contaminate the version diff.

## Phase 3: Decouple publishing from generation (optional)

To control publishing on your own schedule rather than on every Fern generation, keep your publish workflow out of Fern's generation cycle:

1. Add the workflow to `.fernignore`:

```text title=".fernignore"
.github/workflows/ci.yml
```

2. Change the workflow trigger from push-to-main to manual or release-tagged:

```yaml title=".github/workflows/ci.yml"
# Instead of:
on:
push:
branches: [main]

# Use:
on:
workflow_dispatch:
release:
types: [published]
```

3. Update any secret references if you renamed credentials in Phase 1.

## Phase 4: First generation with replay

1. Run `fern generate --group <group-name>` from the config repo (or trigger via your existing CI).
2. The first run auto-creates `.fern/replay.lock`. Replay tracks customer commits to generated files from this point on.
3. Review the resulting PR. It contains:
- `[fern-generated]`: pure generator output
- `[fern-replay]`: patches re-applied (empty on first run, since no patches exist yet)
- Updated `.fern/replay.lock`
4. Squash-merge the PR. The next generation re-anchors correctly even after the squash.

## Phase 5: Validate

After the first generation and merge:

- A PR was created (not a direct push to `main`).
- No unintended package release was triggered.
- SDK code is correct and passes CI.
- `.fern/replay.lock` exists in the SDK repo.
- `.fernignore` contains `replay.lock` (and `replay.yml`).

After your first real customization, verify Replay's behavior end-to-end:

- You edit a generated file, commit, and merge to `main`.
- The next `fern generate` detects the patch (PR body shows `patches detected: 1`).
- The customization survives regeneration via a clean 3-way merge.
- `.fern/replay.lock` shows the patch stored.

## Rollback

Replay never modifies files destructively. Your `main` branch always has correct code, so rolling back is straightforward.

**Quick disable.** If you only want to stop Replay and stay in `pull-request` mode, delete `.fern/replay.lock` from the SDK repo and commit. The next `fern generate` runs without Replay; PRs land in the same shape as before, just without the `[fern-replay]` commit.

**Full revert to `release` or `push` mode:**

1. Switch `mode` back in `generators.yml`:

```yaml title="generators.yml" {3}
github:
repository: customer/python-sdk
mode: release # or mode: push
```

2. Re-add publish credentials to the config repo (if you removed them in Phase 1).
3. Clean up Replay state in the SDK repo:
- Delete `.fern/replay.lock`
- Delete `.fern/replay.yml`
- Delete the `fern-generation-base` tag if it exists
4. Close any open `fern-bot` PRs from PR mode.

## Escape hatches

- `fern generate --no-replay`: skip patch application for one generation. Generation still creates `[fern-generated]`; patches aren't applied. Useful for debugging.
- `fern replay forget <pattern>`: untrack specific patches. The next generation overwrites those files.
- `fern replay reset`: delete the lockfile entirely. Your code stays, but Replay loses all state. Run `fern replay bootstrap` to start fresh.

## Known caveats

- **Closed-without-merge replay PRs.** If a replay PR is closed without merging, the next generation detects the abandoned state and re-anchors automatically. No manual cleanup needed.
- **Force pushes and rewritten history.** Replay handles force-pushed branches via a fallback detection path. Existing patches still apply correctly.
10 changes: 7 additions & 3 deletions fern/products/sdks/sdks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ navigation:
- page: Adding custom code
path: ./custom-code.mdx
slug: custom-code
- page: Migrating to Replay
hidden: true
path: ./replay-migration.mdx
slug: replay-migration
- page: Capabilities
path: ./capabilities.mdx
slug: capabilities
Expand Down Expand Up @@ -66,7 +70,7 @@ navigation:
path: ./generators/python/configuration.mdx
slug: configuration
- page: Adding custom code
path: ./generators/python/custom-code.mdx
path: ./generators/python/custom-code.mdx
slug: custom-code
- page: Dynamic authentication
path: ./generators/python/dynamic-authentication.mdx
Expand All @@ -90,7 +94,7 @@ navigation:
path: ./generators/go/configuration.mdx
slug: configuration
- page: Adding custom code
path: ./generators/go/custom-code.mdx
path: ./generators/go/custom-code.mdx
slug: custom-code
- changelog: ./generators/go/changelog
slug: changelog
Expand Down Expand Up @@ -264,7 +268,7 @@ navigation:
path: ./deep-dives/self-hosted.mdx
slug: self-hosted
- section: Reference
contents:
contents:
- page: generators.yml
path: ./reference/generators-yml-reference.mdx
slug: generators-yml
Expand Down
Loading