Reviewing API changes used to mean checking out the branch, regenerating the docs, hosting them somewhere, and praying nobody else opens a different PR before yours merges. PR preview deploys collapse that into a single workflow line: push the spec from CI with --preview, your reviewer clicks a link, the preview expires when the PR closes.
outworx push openapi.yaml \
--project payments-api \
--preview \
--pr 247 \
--commit $GITHUB_SHA \
--branch $GITHUB_HEAD_REFThat's the whole flow. The result is a hosted preview at /<slug>/preview/<pr> with an amber PREVIEW banner showing the PR number, branch, and commit SHA — so reviewers can't accidentally cite a preview URL as the live docs. This guide covers when to use it, how to wire it into GitHub Actions, the lifecycle semantics, and the gotchas.
What a preview deploy actually is
A preview is a regular project_versions row with is_preview = true plus three pieces of PR metadata: the PR number, the commit SHA, and the branch name. Outworx treats it differently than your release versions in five specific ways:
- Hidden from listings.
GET /api/v1/projects/<slug>/versionsexcludes preview rows by default. Pass?include_previews=truefor admin tooling. The public docs version dropdown also hides them — your customers see your shipped versions, your reviewers see the PR's version. - Never the default. Even if you push a preview as the only version, it won't get promoted. The single-default invariant only applies to non-preview versions.
- Separate plan cap. Preview versions count against
maxPreviewsPerProject(5 on Pro, 20 on Business), not the regularmaxVersionscap. Previews can't crowd out release versions. - 14-day rolling TTL. Each push pushes the expiry forward by 14 days. A slow review keeps its preview; an abandoned PR auto-cleans up. Pruning runs in
pg_cronat 03:23 UTC. - Re-pushes update the same row. Push the same preview label again on a later commit and the existing row updates (TTL rolls forward, embeddings + cached MCP types invalidate). No 409, no drift previews piling up.
Wiring it into GitHub Actions
The smallest functional workflow:
name: Docs preview
on:
pull_request:
paths:
- 'openapi.yaml'
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: outworx/upload-spec-action@v1
with:
token: ${{ secrets.OUTWORX_TOKEN }}
project: payments-api
spec: openapi.yaml
preview: true
pr: ${{ github.event.pull_request.number }}
commit: ${{ github.event.pull_request.head.sha }}
branch: ${{ github.event.pull_request.head.ref }}Three things matter here:
pathsfilter keeps the workflow from re-running when unrelated files change. Without it, every commit on the PR republishes — fine, but wasteful.preview: trueis the toggle. Without it, the action does a regular release-version push.- The token must be a personal access token (
otwx_pat_*). Project upload tokens (otwx_*) don't support preview semantics — the CLI rejects the combo upfront with a clear error message.
If you also want a comment on the PR linking to the preview URL, add a follow-up step:
- name: Comment with preview URL
uses: actions/github-script@v7
with:
script: |
const url = `https://docs.outworx.io/payments-api/preview/${context.issue.number}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `📚 Docs preview: ${url}`,
});Other CI providers
The CLI works anywhere Node 20+ runs. The flag shape is identical; only the env var lookup changes:
# GitLab CI
outworx push openapi.yaml --project payments-api --preview \
--pr "$CI_MERGE_REQUEST_IID" \
--commit "$CI_COMMIT_SHA" \
--branch "$CI_COMMIT_REF_NAME"
# CircleCI
outworx push openapi.yaml --project payments-api --preview \
--pr "$CIRCLE_PULL_REQUEST" \
--commit "$CIRCLE_SHA1" \
--branch "$CIRCLE_BRANCH"
# Buildkite
outworx push openapi.yaml --project payments-api --preview \
--pr "$BUILDKITE_PULL_REQUEST" \
--commit "$BUILDKITE_COMMIT" \
--branch "$BUILDKITE_BRANCH"Plan limits
The preview cap is a separate dial from regular versions. Pro projects can hold up to 5 active previews; Business up to 20. CI re-pushing the SAME preview label updates the existing row, so the cap only fires when CI tries to open a NEW preview while at the limit. If you hit it, close a stale PR (or delete its preview row from the dashboard) and re-run.
The 14-day TTL is rolling, not absolute — every push pushes the expiry forward. So a long-running PR keeps its preview as long as commits keep landing. Once the PR closes (merge or cancel) and no further pushes come in, the preview expires on its own and the prune cron sweeps it.
What happens at merge
Merging the PR doesn't automatically promote the preview. The preview keeps its preview status — you push a regular release version (without --preview) on the merge commit if you want it to become the default:
on:
push:
branches: [main]
paths:
- 'openapi.yaml'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: outworx/upload-spec-action@v1
with:
token: ${{ secrets.OUTWORX_TOKEN }}
project: payments-api
spec: openapi.yaml
# No preview flag → regular release pushThis split is intentional. A merged PR isn't the same as a release — the merge commit might land alongside other unrelated work, the docs version might lag the code release by a day, etc. Keeping the release push as a separate workflow file gives you that control.
Gotchas
- Branch name length. GitHub branches max out around 244 chars; the preview metadata caps a hair higher (256). Truncate or skip the
--branchflag for unusual setups. - Commit SHA format. The CLI accepts 7-64 hex chars. GitHub's
$GITHUB_SHAis the full 40-char SHA. CI variables that give you a short SHA ($CIRCLE_SHA1is full,$BITBUCKET_COMMITis short) both work. - Preview URL in the banner. The banner links back to the GitHub PR thread when the metadata is present. If you skip the
--branchor--commitflags, the banner still renders but the link target is the docs preview itself. - Embeddings + MCP types. Each push invalidates the version's embedding index and any cached generated TypeScript types. If you have an MCP client connected to the preview version, it'll fetch fresh types on the next call.
- Audit log entry. Every preview push writes a
PREVIEW_CREATEDrow to the audit log on Business plans, with the PR / commit / branch inmetadata. Useful for forensic traces ("which CI run pushed this preview?") via themetadata.token_idfield.
When to reach for it vs alternatives
PR previews are the right tool when reviewers need to see the rendered docs, not just the spec diff. If your team's review flow is "look at the YAML changes, approve" you can probably get by with Spec Diff alone — it posts a sticky comment classifying every change as breaking / additive / cosmetic.
PR previews are most valuable when:
- You ship customer-facing API docs and want product / support / partners to review the rendered output
- Your changes touch markdown overrides or per-endpoint customizations (those don't show up in a spec diff)
- You want a stable URL to share with an enterprise prospect mid-review
You can run both in parallel. The combination — diff comment + clickable preview URL — is what most production-grade API teams ship.
Now wire it up.
Related guides
Outworx REST API — Manage Projects from CI
Drive every Outworx Docs action from a script: create projects, push spec versions, promote a default. Personal access tokens, copy-paste curl, and the full OpenAPI spec.
Outworx CLI — push specs from anywhere
Publish OpenAPI / Swagger / GraphQL specs to Outworx Docs from your laptop or any CI provider. One command, zero install graph, scriptable exit codes.
Free Hosted Mock Server for Every API Spec
Every OpenAPI, Swagger, and GraphQL spec on Outworx Docs gets a free hosted mock URL on upload — with stateful overrides, templating, validation, and chaos.
Ship your API docs in under a minute.
Upload your OpenAPI, Swagger, or GraphQL spec and get beautiful hosted docs with AI chat and a per-project MCP server — free forever for 2 projects.