@webflow/webflow-cli
v2.8.0
Published
The Webflow CLI lets you manage Webflow sites, CMS content, forms, assets, and code components from the command line.
Downloads
328,913
Maintainers
Keywords
Readme
@webflow/webflow-cli
The Webflow CLI lets you manage Webflow sites, CMS content, forms, assets, and code components from the command line.
Installation
npm install -g @webflow/webflow-cliCommands
- Authentication
- Sites
- CMS
- Forms
- Assets
- DevLink Import (Code Components)
- DevLink Export
- Designer Extensions
- Apps (Webflow Cloud)
- Skills (agent skills)
- Global Options
Authentication
Authenticate with Webflow and save credentials locally.
webflow auth login # OAuth login — saves token to .env
webflow auth login --force # Re-authenticate even if credentials existSites
Manage Webflow sites from the CLI, and keep a project directory's local source files (page bodies, styles, variables) in sync with the Designer.
webflow sites list # List all sites in your workspace
webflow sites get <siteId> # Get details for a specific site
webflow sites domains --site <siteId> # List custom domains for a site
webflow sites publish --site <siteId> # Publish a site
webflow sites init # Bind this project to an existing site, or create a new one
webflow sites init --site-id <siteId> # Bind without the interactive picker (CI/scripted setup)
webflow sites init --create --name "My Site" # Create a new site and bind to it
webflow sites pull # Write the site's source files locally
webflow sites push # Push local changes back to the site
webflow sites push --dry-run # Preview what push would send
sites init,sites pullandsites pushare in beta and ship only in@nextbuilds (npm i -g @webflow/webflow-cli@next). They aren't registered in the stable release yet.
By default, pages materialize under src/pages/<slug>.tsw (the default page as index.tsw), components under src/components/<name>.tsw, and design-system theme modules under src/theme/. Styles and variables have no framework convention to follow, so they land at site/styles/ and site/variables/. A .webflow/wfs/ lockfile (one file per tracked path, plus an index.json) records which site the project is synced with and each file's last-synced hash, so push can detect conflicting Designer edits.
Set site.layout in webflow.json to use a different framework's layout:
{
"siteId": "...",
"site": {
"layout": { "preset": "nextjs" }
}
}| Preset | Shape |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| library/astro | Flat src/pages/<slug>.tsw + src/components/<name>.tsw (the default) |
| nextjs | Next.js App Router — one folder per route segment, nested page.tsw (src/app/<slug>/page.tsw, home at src/app/page.tsw) |
| nextjs-pages | Next.js Pages Router — flat pages/<slug>.tsw |
A page nested under a Designer Page Folder (e.g. /blog/en/about) mirrors that subpath locally (<pagesRoot>/blog/en/about.tsw, or .../blog/en/about/page.tsw under the nextjs shape). A component's Designer group maps to a local subfolder under componentsRoot — src/components/ui/Card.tsw and a flat src/components/Card.tsw are distinct components. Explicit pagesRoot/componentsRoot/pagesShape ("flat" or "nested") override a preset's defaults, or can be set without a preset at all. The theme root isn't configured directly — it's derived as a sibling of whatever pagesRoot and componentsRoot have in common, so src/pages + src/components puts theme modules at src/theme.
Pulled .tsw files are formatted with Prettier, honoring the project's own Prettier config when it has one.
A pull also writes reference material the site regenerates each time — site/README.md, WFDL typedefs under site/wfdl/, and site/assets.json. These are read-only: the site tells the CLI which paths it accepts writes to, and push skips the rest whether or not --force is passed. Edit them freely for local reference, but a pull will overwrite your changes and a push will never send them.
A project is bound to one site: the lockfile records which site it was pulled from, and both commands refuse to run against a different one. Use sites pull --site-id <other> --reset to repoint a project at another site — it discards the existing sync state and pulls from scratch.
sites list options
| Flag | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------- |
| --fields <fields> | Comma-separated columns to display (id, displayName, shortName, timeZone, createdOn, lastUpdated, lastPublished) |
| --json | Output as JSON |
sites publish options
| Flag | Description |
| --------------------- | ---------------------------------------------------------- |
| --site <siteId> | Site ID to publish |
| --domain <domainId> | Publish to a specific custom domain (repeatable) |
| --page <pageId> | Publish a single page instead of the full site |
| --dry-run | Preview what would be published without triggering a build |
sites init options
| Flag | Description |
| --------------------- | --------------------------------------------------------------- |
| --site-id <siteId> | Skip the interactive picker and bind to this site ID |
| --create | Create a new Webflow site instead of binding to an existing one |
| --name <name> | Name for the new site (implies --create; skips the prompt) |
| --api-token <token> | Use this token instead of the stored session |
sites pull options
| Flag | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| --site-id <siteId> | Pull this site instead of the one in webflow.json, without rebinding the project. Needs --reset if the project is already synced with another site |
| --force | Overwrite local files that changed both locally and in the Designer since the last sync (they're left alone by default) |
| --reset | Discard the existing local sync state and pull from scratch |
| --json | Output the pulled manifest and file list as JSON |
| --api-token <token> | Use this token instead of the stored session |
sites push options
| Flag | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------- |
| --dry-run | List the files that would be pushed without sending them |
| --force | Skip upstream conflict checks and re-push every writable tracked file, not just the ones that changed locally |
| --json | Output the push results as JSON (adds a skipped object when files were left out) |
| --api-token <token> | Use this token instead of the stored session |
When --json leaves files out, the envelope carries a skipped object alongside results (or alongside changes under --dry-run, which has no results key). It appears only when something was skipped, and carries just the keys that apply:
| Key | Shape | Meaning |
| ------------------ | ------------------------------ | --------------------------------------------------------- |
| readOnly | local paths | Read-only reference files with local edits |
| missing | local paths | Tracked files no longer on disk |
| stale | local paths | Leftover copies of files that now live elsewhere |
| unmappable | local paths | Files that don't resolve to a pushable wire path |
| orphanedUpstream | local paths | Files deleted in the Designer that still have local edits |
| renameCandidates | { oldPath, newPath } objects | Files that look like a local rename |
renameCandidates holds objects while every other key holds plain path strings, so flattening Object.values(skipped) into one list of paths will not work.
CMS
Manage CMS collections and items.
# Collections
webflow cms collections list --site <siteId>
webflow cms collections get --site <siteId> --collection <collectionId>
webflow cms collections create --site <siteId> --name "Blog Posts" --slug "blog-posts"
# Fields
webflow cms fields create --collection <collectionId> --name "Author" --type PlainText
# Items
webflow cms items list --site <siteId> --collection <collectionId>
webflow cms items get --site <siteId> --collection <collectionId> --item <itemId>
webflow cms items create --site <siteId> --collection <collectionId> --data '{"name":"Hello"}'
webflow cms items update --site <siteId> --collection <collectionId> --item <itemId> --data '{"name":"Updated"}'
webflow cms items delete --site <siteId> --collection <collectionId> --item <itemId>
webflow cms items publish --site <siteId> --collection <collectionId> --item <itemId>cms collections list options
| Flag | Description |
| ------------------- | ---------------------------------------------------------------------- |
| --sort-by <field> | Sort by: displayName, slug, lastUpdated (default: displayName) |
| --order <dir> | asc or desc (default: asc) |
| --json | Output as JSON |
Forms
List forms and export submissions from a Webflow site.
webflow forms list --site <siteId> # List all forms on a site
webflow forms submissions --site <siteId> --form <formId> # List submissions in the terminal
webflow forms submissions --site <siteId> --form <formId> --output out.csv # Export submissions to CSVNote: Forms commands require the
forms:readOAuth scope. If you authenticated before upgrading to v1.21.0, runwebflow auth login --forceto pick up the new scope.
forms list options
| Flag | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| --site <siteId> | Site ID (required) |
| --fields <fields> | Comma-separated columns: id, displayName, name, slug, createdOn, lastUpdated (default: id,displayName,slug) |
| --json | Output as JSON |
forms submissions options
| Flag | Description |
| ----------------- | ---------------------------------------------------- |
| --site <siteId> | Site ID (required) |
| --form <formId> | Form ID (required) |
| --output <file> | Save all submissions as CSV to the specified file |
| --json | Output as JSON (ignored when --output is also set) |
Assets
Upload and manage site assets.
webflow assets list --site <siteId> # List assets
webflow assets upload <file> --site <siteId> # Upload a file
webflow assets update <assetId> --display-name "New name" # Update asset metadata
webflow assets folders --site <siteId> # List asset folders
webflow assets folders create --site <siteId> --name "Images"assets list options
| Flag | Description |
| --------------------- | --------------------------------------------------------------------------------------------------------- |
| --folder <folderId> | Filter by folder |
| --sort-by <field> | Sort by: displayName, originalFileName, contentType, size, lastUpdated (default: displayName) |
| --order <dir> | asc or desc (default: asc) |
| --fields <fields> | Comma-separated columns to display |
| --limit <n> | Max assets to return |
| --offset <n> | Skip N assets (for pagination) |
| --json | Output as JSON |
assets upload options
| Flag | Description |
| ---------------------- | ----------------------------------- |
| --name <displayName> | Display name (defaults to filename) |
| --folder <folderId> | Upload into a folder |
| --dry-run | Preview without uploading |
Supported file types: PNG, JPG, JPEG, GIF, SVG, WEBP, AVIF (≤ 4 MB); PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, CSV, ODT, ODS, ODP, JSON, Lottie (≤ 10 MB)
DevLink Import (Code Components)
Build and import React component libraries to your Webflow workspace.
webflow devlink import # Build and share your component library
webflow devlink bundle # Build locally without sharingAliases: webflow library share, webflow library bundle
Prerequisites
Create a webflow.json in your project root:
{
"library": {
"name": "My Component Library",
"description": "A collection of custom components",
"components": ["src/components/**/*.webflow.tsx"]
}
}devlink import options
| Flag | Description |
| --------------------- | --------------------------------------------------- |
| --api-token <token> | Workspace API token (overrides WEBFLOW_API_TOKEN) |
| --force | Compile even with warnings or type errors |
| --dev | Bundle in development mode |
devlink bundle options
| Flag | Description |
| ---------------------- | ----------------------------------------------------- |
| --public-path <path> | Set the public path for the bundle |
| --output-path <path> | Set the output path for the bundle (default ./dist) |
| --force | Compile even with warnings or type errors |
| --dev | Bundle in development mode |
DevLink Export
Export Webflow Components from your Webflow Site
webflow devlink export # Export Webflow Componentsdevlink export options
| Flag | Description |
| --------------------- | --------------------------------------------------- |
| --api-token <token> | Workspace API token (overrides WEBFLOW_API_TOKEN) |
| --site <siteId> | Site ID to export from |
Designer Extensions
Scaffold and serve Webflow Designer Extensions.
webflow extension init <name> # Create a new extension project
webflow extension serve [port] # Serve locally (default port: 1337)
webflow extension bundle # Build and package for upload (outputs bundle.zip)Apps (Webflow Cloud)
Manage Webflow Cloud app environment variables non-interactively. --app-id and
--environment-id default from webflow.json (cloud.app_id / cloud.environment_id)
when omitted.
webflow apps env-vars list --app-id <appId> --environment-id <envId> # List variables
webflow apps env-vars set <key> <value> --app-id <appId> --environment-id <envId> # Create or update (plaintext; for secrets use stdin/prompt below)
webflow apps env-vars set <key> --secret # Prompt (hidden) for a secret value
printf %s "$VALUE" | webflow apps env-vars set <key> --secret # Read a secret from stdin
webflow apps env-vars delete <key> --app-id <appId> --environment-id <envId> # Delete a variable
webflow apps env-vars import .env --app-id <appId> --environment-id <envId> # Bulk import from a .env file
webflow apps env-vars import .env.secrets --secret # Import every key as a secretSecret values are encrypted server-side and are never displayed: reads return
them masked, and set / import never echo values to stdout, logs, or
telemetry.
For set, the value is optional: omit it to read from piped stdin or to be
prompted with hidden input. Prefer this for secrets — passing a value as a
command-line argument leaves it in your shell history and the process list
(set warns when you do so with --secret).
env-vars list options
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| --app-id <id> | Cloud app ID (default: cloud.app_id in webflow.json) |
| --environment-id <id> | Environment ID (default: cloud.environment_id in webflow.json) |
| --fields <fields> | Comma-separated columns (key, isSecret, value, id, environmentId). value is excluded by default |
| --json | Output as JSON |
env-vars set / delete / import options
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------ |
| --app-id <id> | Cloud app ID (default: cloud.app_id in webflow.json) |
| --environment-id <id> | Environment ID (default: cloud.environment_id in webflow.json) |
| --secret | (set, import) Mark the variable(s) as an encrypted secret |
| --dry-run | Preview without making any API calls |
| --json | Output as JSON |
A plain set on an existing secret preserves its secrecy (it never silently
downgrades a secret to plaintext). import reports a per-key
created/updated/skipped/errors breakdown and exits non-zero if any key fails
(e.g. a reserved or invalid name). The bulk import is capped at 100 variables.
Skills (agent skills)
Install Webflow's first-party agent skills into your coding agents, so an agent working in your repo knows how to use Webflow without being told each time.
These commands write outside the current project. A
--globalinstall writes into your coding agents' skill directories under your home directory. Two rules bound that: nothing is replaced or deleted unless the CLI wrote it and it has not been edited since (anything else needs--force), and--dry-runprints every path before a single byte is written. Run it first if you are unsure.
webflow skills install # Restore from a lockfile, or pick interactively
webflow skills install webflow-cms --dry-run # Preview every path that would be written
webflow skills install webflow-cms --agent claude-code,cursor
webflow skills install --all-skills --all-agents --global
webflow skills list # What is installed, and where
webflow skills update --check # Report what would change; write nothing
webflow skills update
webflow skills remove webflow-cms --dry-run # Preview exactly what is deletedScope: project or global
| Scope | Where skills go | Lockfile |
| ------------------- | ------------------------------------------- | ---------------------------------------- |
| Project (default) | Agent directories under the current project | webflow-skills-lock.json (commit this) |
| Global (--global) | Agent directories under your home directory | skills-lock.json in the CLI config dir |
The project lockfile is meant to be committed: a teammate (or CI) runs
webflow skills install with no arguments and gets the same skills at the same
commit. list shows both scopes by default so a global install is never
invisible.
Each skill's files are written once into a shared canonical store and linked into
every selected agent's directory, so installing for twenty agents costs one copy.
Symlinks are used by default, with an automatic per-target fallback to copying
when the filesystem or platform refuses them; --copy forces copies everywhere.
Safety
install and update refuse to overwrite a skill directory unless the CLI
wrote it and it has not been edited since — anything locally modified, or
anything that exists without a matching lockfile entry, is reported as blocked
and needs --force. --dry-run prints the exact absolute paths that would be
written or deleted and makes no changes, which is the flag to reach for first
when an agent chose the arguments.
skills install options
| Flag | Description |
| --------------------- | ---------------------------------------------------------------------- |
| -a, --agent <ids> | Target agent; repeatable, comma-separated, or all |
| --all-agents | Install for every supported agent |
| --all-skills | Install every skill in the catalog |
| -g, --global | Install into your home directory instead of the current project |
| -p, --project | Install into the current project (the default) |
| --copy | Copy files instead of symlinking from the shared store |
| --ref <branch\|sha> | Pin to a branch or 40-character commit SHA (default: main) |
| --force | Replace destinations that were modified locally or installed elsewhere |
| -y, --yes | Skip the confirmation prompt |
| --dry-run | Print every path that would be written; change nothing |
| --json | Output as JSON |
skills update options
| Flag | Description |
| --------------------- | ------------------------------------------------- |
| --check | Report what would change without writing anything |
| -g, --global | Update the home-directory install |
| -p, --project | Update the current project's install |
| --ref <branch\|sha> | Re-pin to a different branch or commit SHA |
| --force | Overwrite skills that were modified locally |
| -y, --yes | Skip the confirmation prompt |
| --dry-run | Preview without writing |
| --json | Output as JSON |
skills list options
| Flag | Description |
| ------------------- | ----------------------------------------------------------------------------- |
| -g, --global | Only the home-directory install |
| -p, --project | Only the current project's install |
| -a, --agent <ids> | Only skills linked into these agents; repeatable and comma-separated |
| --fields <fields> | Comma-separated columns (name, dir, description, status, mode, agents, scope) |
| --sort-by <field> | Sort by name, dir, status, or scope (default: name) |
| --order <dir> | asc or desc (default: asc) |
| --json | Output as JSON |
status is computed offline by re-hashing what is on disk against what the
lockfile recorded: up-to-date, modified (edited since install), missing
(recorded but gone), or unmanaged (present with no lockfile entry). Checking
for upstream changes needs the network and is skills update --check's job.
skills remove options
| Flag | Description |
| ------------------- | ------------------------------------------------------------- |
| -a, --agent <ids> | Only unlink from these agents; repeatable and comma-separated |
| --all | Remove every installed skill for the scope |
| -g, --global | Remove from the home-directory install |
| -p, --project | Remove from the current project's install |
| --force | Also remove skill directories that have no lockfile entry |
| -y, --yes | Skip the confirmation prompt |
| --dry-run | Print exactly what would be deleted; change nothing |
| --json | Output as JSON |
Removing a skill from one agent unlinks it there and keeps the shared store as long as another agent still references it; the store directory is deleted once nothing does.
remove only deletes what the lockfile records. A skill directory with no
lockfile entry was not installed by this CLI — it may belong to another tool, or
be hand-written — so --all leaves it alone and names it in a warning, and
naming it directly is refused. --force opts in to removing those too.
Global Options
Available on all commands:
| Flag | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------- |
| --no-input | Disable interactive prompts (for CI/CD) |
| --manifest <path> | Path to webflow.json (default: ./webflow.json) |
| --skip-update-check | Skip checking for package updates |
| --verbose | Show detailed output |
| --region <us\|eu> | Webflow region to target. See Region targeting below. Beta (@next) only for now. |
JSON errors
Commands run with --json emit failures as one JSON document on stdout and
exit non-zero. The error object always contains message and may add fields
that callers can use without parsing prose:
{
"error": {
"message": "The filesystem schema changed.",
"context": "Failed to push site files",
"missingFlag": "site",
"code": "SCHEMA_STALE",
"diagnostics": [
{
"source": "wfs",
"phase": "conflict",
"code": "SCHEMA_STALE",
"severity": "error",
"message": "The filesystem schema changed.",
"hints": [],
"retryable": false
}
]
}
}context, missingFlag, code, and diagnostics are omitted when they do not
apply. WFS diagnostics include stable source, phase, code, severity, source
position, hints, retryability, and recovery metadata when supplied by the API.
Retry-aware commands (currently apps reads) add a further set of fields for
distinguishing why a command failed without parsing the message, matching
the exit code (see Exit codes below):
{
"error": {
"message": "429 Too Many Requests",
"httpStatus": 429,
"retryable": true,
"retryAfterMs": 12000,
"attempts": 3,
"requestId": "req_abc123"
}
}httpStatus— the API's response status, when the failure came from a request the CLI actually sent.retryable— whether this class of failure is one a caller could reasonably retry later. Set when the failure came from the CLI's retry-aware path (i.e. also hasattempts), and additionally on any 429 — including from a write command — since the rate limiter rejects the request before it reaches the route handler, so nothing was applied server-side. A write command's non-429 failures never set this, since retrying a non-idempotent write automatically isn't safe.retryAfterMs— the wait, in milliseconds, before a retry is likely to succeed. Present only alongsideretryable: true. A value derived from a server header is exact; one derived from the CLI's own backoff (no header present) is a lower bound, floored at the default policy's base backoff delay (1000ms), so it may be conservative.attempts— how many HTTP attempts the CLI made before giving up (including the first).requestId— a correlation id echoed by the API, for support requests. Not present on every failure — the general rate limiter can reject a request before the id is attached to the response.authError— set on the CLI's own auth-resolution failures (no session, or a failedwebflow auth loginflow) rather than an API rejecting a token — there is nohttpStatusto key off in that case, but the fix is identical either way: runwebflow auth login. A missing--sitein a no-input run isisUserError/missingFlaginstead (exit 2), notauthError, since the token can be valid — only the site is unresolved.
All of the fields above are omitted when they do not apply.
Exit codes
| Code | Meaning |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | Success (including an empty result) |
| 1 | General/unspecified error |
| 2 | User-recoverable error — bad input, a missing required flag, a resource locked by another process, and similar states the caller can address without a backend change |
| 3 | Auth error — either the API rejected a token (httpStatus 401/403), or the CLI's own auth resolution failed before any request was made (authError). Either way, the fix is webflow auth login. |
| 4 | Rate limited (httpStatus 429) |
| 5 | Transport error — an exhausted 5xx/408 retry, or a network failure |
| 130 | Interrupted (Ctrl-C) |
A script or agent that only checks $? -ne 0 sees no difference from prior
CLI versions; one that specifically checked $? -eq 1 to mean "any failure"
should switch to $? -ne 0 to keep matching every failure.
Region targeting
US and EU are fully isolated Webflow stacks with separate accounts — the region in force decides which hosts and OAuth application every command uses. Resolved in this order, highest priority first:
--region <us|eu>flagWEBFLOW_REGIONenvironment variable (the CI/CD equivalent of the flag)regionfield inwebflow.json- The region you last logged into (persisted in
~/.config/webflow/auth.json) us(default)
{
"region": "eu"
}Only one session is stored at a time. webflow auth login authenticates
against the active region's OAuth application and makes that region the
default for later commands that don't specify one; logging into a different
region replaces the stored session and updates the default. webflow auth
status shows the active region and whether it has a stored session.
webflow auth logout clears the stored session but keeps the region as the
default, so a later regionless webflow auth login returns to it.
This is currently gated to @next (beta) and local development builds — EU
targeting isn't announced yet. webflow --region eu ... on a stable install
returns "unknown option."
License
MIT
