Agents & CI/CD

Drive the Webflow CLI from AI agents, scripts, and CI/CD pipelines without a terminal.

The Webflow CLI is built to run unattended — from an AI coding agent’s subprocess, a shell script, or a CI/CD job. This guide covers the non-interactive contract every command follows: which flags to pass, how errors and exit codes behave, and how to configure and operate a Webflow Cloud app end to end. For the full flag reference of every command, see the command reference.

apps commands need the @next channel

The webflow apps namespace used throughout this guide ships only on the CLI’s pre-release channel. Install it explicitly — on the stable channel these commands do not exist and fail with an unknown-command error:

$npm install -g @webflow/webflow-cli@next

A pre-release install reports a version with a -next. suffix (webflow --version). The stable channel covers webflow cloud init and webflow cloud deploy only; there is no stable equivalent for the management commands.

Why this matters

The CLI’s interactive prompts (picking a site, naming an app, confirming a delete) only fire when the process has an attached TTY. An agent invoking the CLI through a subprocess — or a CI runner — has no TTY, so every prompt is silently skipped. If a required value wasn’t also passed as a flag, the command doesn’t hang waiting for input: it fails immediately with an error naming the missing flag.

The rule for every command: pass every required value explicitly as a flag, and add --no-input to make that contract explicit.

$webflow apps deploy --no-input --site-id site_abc123 --mount /app --environment production

--no-input is also auto-detected

You don’t need to pass --no-input yourself. The CLI enables it automatically when CI=true, when stdout or stdin isn’t a TTY, or when it detects a coding agent’s execution environment — which covers most agent and CI runners already. Passing it explicitly is still good practice: it documents intent and behaves the same in every environment, including a developer’s terminal.

If the detection is wrong for your setup — an interactive terminal that trips one of those checks — pass --input to force prompts back on. An explicit --no-input always wins over --input.

Reading errors from a script

Exit codes are semantic and safe to branch on:

CodeMeaning
0Success — including an empty result set (an empty list isn’t an error).
1An operational error: auth failure, API error, invalid input, or a required value that couldn’t be resolved. Also a poll timeout under deployments get --wait.

When a required ID can’t be resolved non-interactively, the CLI fails fast with a single-line message naming the exact flag, environment variable, or webflow.json key to set, for example:

Missing required appId. Pass --app-id, set WEBFLOW_APP_ID, or add it to webflow.json.

This message format is stable and safe to grep for in CI logs. The CLI doesn’t currently wrap it in a JSON envelope, though — even with --json set, error text stays plain, human-readable text on stderr. --json only changes success output (see below) and suppresses incidental info lines like Using appId from webflow.json. Treat the exit code, not stdout/stderr parsing, as the authoritative pass/fail signal; reserve message-text matching for surfacing the reason to a human or a log.

Destructive commands still require confirmation

apps delete refuses to run under --no-input (or --json) unless you also pass --yes — it never silently deletes anything. Preview any destructive or mutating command with --dry-run first; every write command (apps update, apps delete, apps env-vars set/delete/import) supports it and makes zero API calls.

ID resolution

Commands that operate on a specific app, environment, site, or workspace resolve that ID the same way, in strict priority order:

  1. An explicit flag (e.g. --app-id, --site-id) — always wins.
  2. An environment variable.
  3. webflow.json in the current directory (or --manifest <path>).
  4. An interactive prompt — skipped entirely under --no-input, in CI, or without a TTY. If exactly one candidate exists (a workspace with a single app, an app with a single environment), the CLI selects it automatically, even non-interactively. Only an ambiguous choice — several candidates, none specified — produces the missing-flag error above.
ResourceFlagEnvironment variablewebflow.json key
Site--site / --site-idWEBFLOW_SITE_IDsiteId
App--app-idWEBFLOW_APP_IDcloud.app_id (falls back to the legacy cloud.project_id)
Environment--environment-idWEBFLOW_APP_ENVIRONMENT_IDcloud.environment_id
Workspace--workspace-idWEBFLOW_WORKSPACE_IDcloud.workspace_id

WEBFLOW_API_TOKEN is the one exception — it’s read-only from the environment (or .env) and never resolved from webflow.json. See Authentication for how it’s issued.

Environment variables here are read-only inputs — the CLI never writes WEBFLOW_APP_ID or the others back into your .env; only apps init / apps deploy write the equivalent keys into webflow.json. See Configuration for the full webflow.json schema.

Pass IDs explicitly in CI, even when webflow.json has them

Flags always override the manifest, which is useful for recovering from a half-written webflow.json or targeting a different environment for one run without editing a file. In a CI job, prefer --site-id / --app-id / --environment-id flags sourced from secrets over relying solely on a committed webflow.json.

Your first deploy

A Webflow Cloud app deploys one of two ways, chosen once at setup:

  • Site-attached — the app is bound to an existing Webflow site and served from a mount path on that site’s domain (e.g. /app). Pass --site-id.
  • Project app — a standalone app with no existing site. The first deploy provisions a new Webflow site automatically. Pass --new at init and --workspace-id for the first deploy.
$# Site-attached: connect to an existing site
$webflow apps init --no-input \
> --app-name my-app \
> --framework astro \
> --mount /app \
> --site-id site_abc123
$
$webflow apps deploy --no-input \
> --site-id site_abc123 \
> --mount /app \
> --environment production \
> --skip-mount-path-check
$# Project app: no existing site
$webflow apps init --new --no-input \
> --app-name my-app \
> --framework nextjs \
> --workspace-id ws_abc123
$
$webflow apps deploy --no-input \
> --workspace-id ws_abc123 \
> --app-name my-app \
> --mount / \
> --environment production
Pass --mount and --environment together, every time

The deploy prompts (pick an app, name a new app, pick an environment) are gated on whether --mount and --environment are both set — not on --no-input. Passing --no-input without both still triggers the prompt and hangs in a non-TTY context.

Discovering a site ID

Site IDs aren’t secret, but an agent still needs a way to find one without a human copying it from the dashboard:

$webflow sites list --json

Parse the JSON array and match on displayName to present a human-readable choice, rather than asking a user to type a raw ID.

Discovering a workspace ID

Workspace IDs aren’t surfaced anywhere in the Webflow dashboard UI — there’s no page to copy one from. The only reliable way to discover one is running an interactive command locally, once:

$webflow apps deploy

Run without --no-input and without --workspace-id, the preflight prompt walks a human through creating a new app and picking a workspace, then writes cloud.workspace_id into webflow.json. From that point on, an agent can read the value back out of the manifest and pass it as --workspace-id on every subsequent run.

Continuous deployment

Once an app deploys once, there are two ways to keep it deploying automatically:

A one-time, dashboard-only setup — the CLI can’t perform it, and pushing to GitHub alone doesn’t enable it:

1

Push the app to GitHub

The repository needs at least one commit pushed to a remote.

2

Connect the repository in the Webflow dashboard

Open your Cloud app in the Webflow dashboard, go to Settings → Git, connect your GitHub account if needed, then select the repository and the branch to deploy from.

3

Confirm the connection

The dashboard runs one initial deploy automatically to verify the wiring.

From that point on, every git push to the connected branch triggers a deploy — no CLI invocation, no workflow file to maintain.

GitHub Actions

Use this when you need custom build steps, per-environment secrets, or deploy gates the native Git integration doesn’t cover:

.github/workflows/deploy.yml
1name: Deploy to Webflow Cloud
2
3on:
4 push:
5 branches: [main]
6
7jobs:
8 deploy:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12
13 - uses: actions/setup-node@v4
14 with:
15 node-version: 22
16
17 # `apps` commands need the pre-release channel. Pin an exact version in
18 # CI — the `next` tag moves. Use `webflow cloud deploy` with `@latest` to
19 # stay on the stable channel.
20 - name: Install Webflow CLI
21 run: npm install -g @webflow/webflow-cli@next
22
23 - name: Deploy
24 run: |
25 webflow apps deploy \
26 --no-input \
27 --site-id "$WEBFLOW_SITE_ID" \
28 --mount /app \
29 --environment production \
30 --skip-mount-path-check
31 env:
32 WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_API_TOKEN }}
33 WEBFLOW_SITE_ID: ${{ secrets.WEBFLOW_SITE_ID }}

Wait for the deploy to finish, then confirm it succeeded with apps deployments get --wait. It polls until the deployment reaches a terminal status and exits with a status-derived code — useful as a gate before a downstream step like smoke tests or a Slack notification:

$DEP_ID=$(webflow apps deployments list --limit 1 --json | jq -r '.deployments[0].id')
$webflow apps deployments get "$DEP_ID" --wait --timeout 300

The --json output is an envelope, { "deployments": [...], "nextCursor": ... } — not a bare array — so the id is at .deployments[0].id.

Operating apps

Once an app is deployed, the apps namespace covers day-to-day operations without leaving a script. Every command below supports --json, resolves its app/environment per ID resolution, and is documented in full — every flag, default, and example — in the command reference.

Inspecting apps

$webflow apps list --json # all apps in the workspace
$webflow apps get app_abc123 --json # a single app's details
>webflow apps domains app_abc123 --json # live URLs and custom domains
>webflow apps environments list app_abc123 --json

Deployments

$webflow apps deployments list --limit 10 --json
$webflow apps deployments get dep_abc123 --json
$webflow apps deployments get dep_abc123 --wait --interval 10 --timeout 300
$webflow apps deployments list --status failed --limit 1 --json
$webflow apps deployments redeploy dep_abc123 --idempotency-key "$GITHUB_RUN_ID"
$webflow apps deployments trigger --idempotency-key "$GITHUB_RUN_ID"

A deployment’s status is one of starting, building, deploying (in-flight) or success, failed, canceled, unstaged (terminal). --wait polls until a terminal status, then exits 0 on success and 1 on anything else. Note that a poll timeout also exits 1, so a step that needs to tell a failure from a still-running build has to read the status rather than gate on the exit code alone.

redeploy re-runs an existing deployment at the same commit; redeploying an earlier one is how you roll back. trigger takes no deployment ID and builds the environment’s configured branch at its current HEAD. Pass --idempotency-key on either so a retried CI step acknowledges the first enqueue instead of queueing a second build.

Both need a GitHub-connected app

redeploy and trigger build from a connected repository, so an app created by deploying local files with apps deploy is not eligible. Connect one with apps init --import or apps update --github-source.

Logs

$webflow apps deployments list --limit 1 --json # get a depId first
$webflow apps logs build dep_abc123 --since 2026-07-01T00:00:00Z --q "error" --json
$webflow apps logs runtime env_abc123 --limit 200 --json

Both commands paginate; when a page has more results, the output includes a nextCursor to pass back via --cursor.

Environment variables

$webflow apps env-vars list --app-id app_abc123 --environment-id env_abc123 --json
$webflow apps env-vars set NODE_ENV production --app-id app_abc123 --environment-id env_abc123
$webflow apps env-vars delete OLD_KEY --app-id app_abc123 --environment-id env_abc123
$webflow apps env-vars import .env.production --app-id app_abc123 --environment-id env_abc123 --secret

Secret values are masked wherever they’re read back — in the table view and in --json output alike — and env-vars import never echoes plaintext values, even on success. To set a secret without it ever touching shell history or the process list, pipe the value via stdin instead of passing it as an argument:

$printf '%s' "$STRIPE_KEY" | webflow apps env-vars set STRIPE_KEY --secret \
> --app-id app_abc123 --environment-id env_abc123

Updating and deleting apps

$webflow apps update app_abc123 --description "Marketing site backend" --dry-run
$webflow apps delete app_abc123 --dry-run # preview — makes zero API calls
$webflow apps delete app_abc123 --yes --no-input

--dry-run validates inputs (including auth) and prints exactly what would change, without calling the API. Always preview a delete before running it for real — especially when the target ID came from an agent’s own reasoning rather than a human.

Deprecated cloud aliases

webflow cloud init and webflow cloud deploy still work and share identical options with apps init / apps deploy, but are deprecated in favor of the apps namespace. New scripts and CI pipelines should use apps directly. webflow cloud list is also deprecated — it lists scaffold templates for init, not your Cloud apps, so use apps list to list apps and apps init --framework for templates. See the command reference for the full deprecated alias list.