Skip to content

Canvas CLI & component schema

Lookup material for the @drupal-canvas/cli npm package (the canvas command) and the component schema, for the in-platform-rendering path on Source CMS (see choose your path). For step-by-step instruction, use the Canvas quickstart and the Canvas components section.

Covers @drupal-canvas/cli 0.20.1.

Terminal window
# per project (the Nebula scaffold includes it)
npm install @drupal-canvas/cli
# global
npm install @drupal-canvas/cli -g

Projects scaffolded with npx @drupal-canvas/create@latest have it preinstalled; run it as npx canvas <command>.

Two flows, requiring the site’s Canvas OAuth module (canvas_oauth, shipped as a submodule of Drupal Canvas):

  • Interactive login (individual developers): npx canvas login: OAuth 2.0 authorization code + PKCE via the browser. Tokens are stored in ~/.config/drupal-canvas/oauth.json, keyed by site URL, and used automatically.
  • Client credentials (CI/CD, service accounts): an API client with the Client Credentials checkbox and the scopes below, supplied via environment variables. This is a distinct OAuth client from the DRUPAL_* credentials used for the Content API and MCP server; the CLI reads its own CANVAS_* variable names (below), scoped to pushing/pulling components rather than reading content.

Stored login tokens win over environment credentials (verified 2026-07-03). When oauth.json holds tokens for the target site, the CLI uses them and ignores CANVAS_CLIENT_ID/CANVAS_CLIENT_SECRET. If commands fail with You must be logged in to access this resource. despite valid client credentials in the environment, run canvas logout for that site first. On a Source CMS site, the browser login delegates to Acquia ID, and when your Acquia ID lacks access to that particular site, the stored tokens are rejected by its Canvas API while still shadowing your working client credentials. On a site your Acquia ID does have access to, the browser login works end-to-end (both cases verified 2026-07-03).

Scope Grants
canvas:js_component Create and update code components; canvas push uploads built components as js_component config entities
canvas:asset_library Upload the built global CSS and asset bundles; canvas push uploads Tailwind CSS assets as the site’s asset_library

Both scopes are required for the standard push/pull workflow; they are the scopes to add when creating the API client (API > API clients). Additional scopes the CLI requests when the matching features are enabled: canvas:page:create, canvas:page:read, canvas:page:edit (pages), canvas:content_template (content templates), canvas:page_region (global regions), and canvas:brand_kit (fonts, with --include-brand-kit). When CANVAS_SCOPE is unset, the CLI requests the canvas_oauth defaults.

CLI argument Environment variable Description
--site-url CANVAS_SITE_URL Base URL of the Drupal site
--client-id CANVAS_CLIENT_ID OAuth client ID
--client-secret CANVAS_CLIENT_SECRET OAuth client secret (never commit it)
--scope CANVAS_SCOPE Optional space-separated scope list; defaults to the canvas_oauth defaults
CANVAS_ACCESS_TOKEN Optional pre-issued access token; when set, client ID/secret/scope are ignored
CANVAS_JSONAPI_PREFIX Set if the site’s JSON:API prefix differs from the CLI default jsonapi. There is no auto-detection (confirmed in the CLI source 2026-07-03: the value falls back from flag to this variable to the Drupal-core default). Source CMS sites serve JSON:API at /api, so set CANVAS_JSONAPI_PREFIX=api there
--include-brand-kit CANVAS_INCLUDE_BRAND_KIT Optional; include Brand Kit fonts in pull/push (default false)

Precedence, highest first: command-line arguments → environment variables → project .env~/.canvasrc → defaults. ~/.canvasrc is a dotenv-format file (KEY=value lines) holding the same variables as a project .env, for values shared across projects; the CLI loads it first and the project .env after, so local values win (confirmed in the CLI source 2026-07-03).

Property Default Description
componentDir "src/components" Where components live; must be inside aliasBaseDir
pagesDir "pages" Where page specs live
contentTemplatesDir "content-templates" Where content template specs live
aliasBaseDir "src" Base directory for @/ import resolution
outputDir "dist" Build output directory
globalCssPath "src/global.css" Global (Tailwind) CSS entry point
sync.pages / sync.contentTemplates / sync.regions true Include pages / content templates / global regions in pull, push, and reconcile-media

Log in to a Canvas site via browser (OAuth 2.0 authorization code + PKCE); stores tokens in ~/.config/drupal-canvas/oauth.json.

Terminal window
npx canvas login [options]
Flag Description
--site-url <url> Canvas site URL (prompted if omitted)
--client-id <id> OAuth client ID (overrides auto-discovery)
--port <number> Local callback port (default 4444); the API client’s Redirect URIs (configured at API > API clients in the site’s admin UI) must include http://localhost:<port>/callback

On Source CMS, OAuth discovery delegates to Acquia ID with a pre-registered client, so the browser opens an id.acquia.com login, not the site’s own:

$ npx canvas login --site-url https://your-site.example.com
┌ Drupal Canvas login
◇ OAuth configuration discovered
● Opening browser for login...
│ If it does not open automatically:
│ https://id.acquia.com/oauth2/default/v1/authorize?response_type=code&client_id=…&redirect_uri=http%3A%2F%2Flocalhost%3A4444%2Fcallback&…
◆ Credentials stored for https://your-site.example.com
└ Run `canvas push` or `canvas pull` to get started.

The callback wait times out after 5 minutes; the failure output is (captured):

│ Failed
│ ✗ Authorization failed
│ Timed out waiting for OAuth callback after 5 minutes.
└ Login failed

See the authentication caveat before relying on browser-login tokens for pull/push on a Source CMS site.

Remove stored credentials for a site from ~/.config/drupal-canvas/oauth.json.

Terminal window
npx canvas logout [options]
Flag Description
--site-url <url> Site to log out of (prompted if omitted)

Example:

Terminal window
npx canvas logout --site-url https://example.com
Drupal Canvas logout
Credentials removed: https://example.com
Logout completed

Pull components, global CSS, and optional pages, content templates, global regions, and Brand Kit fonts from Drupal to the local filesystem.

Terminal window
npx canvas pull [options]
Flag Description
--site-url <url> / --client-id <id> / --client-secret <secret> / --scope <scope> Credentials (usually from .env)
-d, --dir <directory> Component directory (default: componentDir or src/components)
--include-pages [enabled] / --include-content-templates [enabled] / --include-regions [enabled] Include pages / content templates / global regions (preset true), the inverse of the --no-* flags below
--no-pages / --no-content-templates / --no-regions Exclude pages / content templates / global regions
--include-brand-kit [enabled] Include Brand Kit fonts
--skip-overwrite Skip items that already exist locally
-y, --yes Skip all confirmation prompts (CI-safe)

Example: npx canvas pull --yes --skip-overwrite: non-interactive, pulls only items not present locally.

With --yes and credentials from .env, the plan runs first, then each stage reports what it wrote; components land in componentDir, pages in pages/, regions in regions/, global CSS at src/global.css:

$ npx canvas pull --yes
┌ Drupal Canvas pull
│ Plan
│ Components: 1 pull (1 new)
│ Assets: global CSS pull
│ Pages: 4 pull (4 new)
│ Global regions: 2 pull (2 new)
◇ Pulled components
│ Succeeded: text
◇ Pulled assets
│ Succeeded: global.css
◇ Pulled pages
│ Succeeded: Access denied, Homepage, Not found, …
◇ Pulled global regions
│ Succeeded: footer, header
└ Pull completed

Build and push local components, global CSS, component dependencies (bundled vendor and @/ imports), and optional pages, content templates, global regions, and fonts to Drupal. Components upload as js_component entities (scope canvas:js_component); built CSS assets upload as asset_library (scope canvas:asset_library).

Terminal window
npx canvas push [options]
Flag Description
--site-url <url> / --client-id <id> / --client-secret <secret> / --scope <scope> Credentials (usually from .env)
-d, --dir <directory> Component directory (default: componentDir or src/components)
--include-pages [enabled] / --include-content-templates [enabled] / --include-regions [enabled] Include pages / content templates / global regions (preset true), the inverse of the --no-* flags below
--no-pages / --no-content-templates / --no-regions Exclude pages / content templates / global regions
--include-brand-kit [enabled] Include Brand Kit fonts
-y, --yes Skip confirmation prompts (CI-safe)

For example:

$ npx canvas push --yes --no-pages --no-regions --no-content-templates
┌ Drupal Canvas push
● Local pages were found but excluded by --no-pages. Remove that flag to push them.
│ Plan
│ Components: 1 update
◇ Pushed components
│ Updated: text
◇ Prepared assets
◇ Pushed assets
│ Global CSS (Tailwind CSS build)
└ ✓ Push completed

Two verified caveats on full pushes (everything included):

  • Pulled system pages don’t push back. A full push of a freshly pulled site failed on the Access denied and Not found pages with Unable to update field path for entity "…" (captured); regular pages updated fine. Later stages are then skipped (Not started) and the run ends ✗ Push incomplete. Exclude them (--no-pages) or expect those two failures.
  • Failures abort stage-by-stage. Push validates and builds everything before uploading; lint or build errors mean nothing at all is uploaded. Push lints component imports first; components authored against the pre-drupal-canvas-package layout fail like this:
│ Plan
│ Components: 26 update
│ Pages: 6 update
│ Global regions: 2 update
│ Failed
│ ✗ src/components/button/index.tsx
│ Line 2, Column 1: Utilities were moved into the `drupal-canvas` package. The `@/lib/utils` path is provided by Canvas and cannot be used for local files. (drupal-canvas/component-imports)
│ ✗ src/components/image/index.tsx
│ Line 1, Column 1: Using `next-image-standalone` directly is deprecated. Use the `Image` component from the `drupal-canvas` package instead. (drupal-canvas/component-imports)

A component that imports a site-provided extension module (e.g. import … from 'canvas_forms/useCanvasForm', the alias exposed in-site by a Canvas extension) fails the pre-push build outside the site, and one failing component aborts the whole push:

│ Failed
│ ✗ Build failed
│ Could not resolve entry module "canvas_forms/useCanvasForm".
│ ✗ Push failed

Another captured failure, against a URL that is not a Canvas-enabled site:

│ Failed
│ ✗ Push failed
│ API endpoint not found: /oauth/token
│ Possible causes:
│ • Canvas module is not enabled
│ • Site URL is incorrect
│ • Server is not responding correctly
└ ✗ Push failed

Build local components, vendor dependencies, and Tailwind CSS assets with automatic component discovery. Requires the global CSS file at globalCssPath. Output goes to per-component dist/ directories plus a top-level dist/ with vendor/, local/, Tailwind assets, and a canvas-manifest.json import map.

Terminal window
npx canvas build [options]
Flag Description
-d, --dir <directory> Directory to scan (default: componentDir or src/components)
--alias-base-dir <directory> Base directory for module resolution (default src)
--output-dir <directory> Build output directory (default dist)
-y, --yes Skip confirmation prompts

Example:

Terminal window
npx canvas build --yes
Drupal Canvas build
Discovered components
Built components
Succeeded: my-hero
Built assets
Tailwind CSS
Generated canvas-manifest.json 0 vendor
packages, 0 local imports
Build completed

Validate local components (ESLint with the required rules from @drupal-canvas/eslint-config, including allowed import patterns) and authored pages, content templates, and global regions.

Terminal window
npx canvas validate [options]
Flag Description
-d, --dir <directory> Component directory to validate
--fix Apply available automatic fixes (default false)

Example (failure shows file, position, and rule):

$ npx canvas validate
┌ Drupal Canvas validate
◇ Validated components
│ Failed
│ ✗ src/components/my-hero/component.yml
│ Line 2, Column 14: Directory name "my-hero" does not
│ match machineName "hello-world".
│ (drupal-canvas/component-dir-name)
└ Validation failed

Create a new code component scaffold (component.yml, index.jsx, index.css) in the component directory. Prompts for the directory if no canvas.config.json is present.

Terminal window
npx canvas scaffold [options]
Flag Description
-n, --name <n> Component name (used for directory and metadata)
-d, --dir <directory> Component directory to create the component in

Example:

$ npx canvas scaffold --name my-hero
┌ Drupal Canvas scaffold
◇ Created component
│ Created: my-hero
│ Directory: src/components/my-hero
│ Component metadata: src/components/my-hero/component.yml
│ Source file: src/components/my-hero/index.jsx
│ CSS file: src/components/my-hero/index.css
└ Scaffold completed

Note: as of 0.20.1 the generated component.yml contains machineName: hello-world regardless of --name; set it to the directory name or canvas validate fails (see example above).

Experimental. Pull context for local AI agents from the Drupal site into .agents/drupal-canvas/: prop-sources.json (available field bindings per entity bundle and component), view-modes.json (view modes per entity type and bundle), and a .gitignore (the directory is not meant to be committed).

Terminal window
npx canvas agents-context [options]
Flag Description
--site-url <url> / --client-id <id> / --client-secret <secret> / --scope <scope> Credentials (usually from .env)

The context lands in .agents/drupal-canvas:

$ npx canvas agents-context
┌ Drupal Canvas agents-context
◇ Saved agents context to .agents/drupal-canvas
└ Agents context pulled

Upload external media referenced in local page specs, content templates, and global regions to Drupal as media entities, and update the local specs with the resolved image data and provenance (target_id) so those resources can be pushed.

Terminal window
npx canvas reconcile-media [options]
Flag Description
--site-url <url> / --client-id <id> / --client-secret <secret> / --scope <scope> Credentials (usually from .env)
-d, --dir <directory> Component directory
--include-pages [enabled] / --include-content-templates [enabled] / --include-regions [enabled] Include pages / content templates / global regions (preset true), the inverse of the --no-* flags below
--no-pages / --no-content-templates / --no-regions Exclude pages / content templates / global regions
-y, --yes Skip confirmation prompts

The nothing-to-do path, immediately after a fresh pull:

$ npx canvas reconcile-media --yes
┌ Drupal Canvas reconcile media
● No unreconciled media found.
└ Media reconciliation skipped

Both commands were removed in favor of pull and push. Running them prints the pointer and exits non-zero:

$ npx canvas download
┌ Drupal Canvas download
▲ The `canvas download` command has been removed.
Use `canvas pull` instead.
└ Command removed

canvas upload prints the same for canvas push.

A code component is a directory (its name is the component’s machine name) containing:

  • component.yml: metadata and schema.
  • The source file with the default export: index.jsx (canvas pull writes TypeScript components as index.tsx, verified 2026-07-03).
  • index.css: optional component CSS.
  • mocks.json: optional preview data for Canvas Workbench.

On the server, component.yml maps onto the Canvas module’s JavaScriptComponent config entity; the canonical schema is the canvas.js_component.* entry in that module’s config schema. The shape below matches components generated by canvas scaffold 0.20.1 and the Nebula template’s example components.

Field Type Constraints
name string Human-readable label shown in the component library
machineName string Must equal the component’s directory name (drupal-canvas/component-dir-name rule)
status boolean Whether the component is available for placement
required array of strings Prop names that must have a value
props object JSON Schema object (see below)
slots object or [] Map of slot definitions (see below)

Prop definitions (props.properties.<propName>)

Section titled “Prop definitions (props.properties.<propName>)”

Each entry is a JSON Schema property; the component receives it as a camelCase argument of the same name.

Field Type Use
type string JSON Schema type: string, integer, number, boolean, object (e.g. image)
title string Field label in the editor’s Props tab
description string Help text in the editor
examples array Example value(s) used for previews
enum array Allowed values (renders as a dropdown)
meta:enum object Human-readable label per enum value
format string String format: uri, uri-reference (links)
$ref string Shared definition, e.g. json-schema-definitions://canvas.module/image for image props (src, alt, width, height)
contentMediaType string text/html marks a formatted-text prop
x-formatting-context string Formatting context for formatted text, e.g. block

Prop types offered in the Canvas editor UI: Text, Formatted text, Link, Image, Video, Date and time, Integer, Number, Boolean, List: text, List: integer, and Content entity reference.

Multi-value (“Allow multiple values”, delivered to the component as an array) can be switched on in the editor for every type except Formatted text, Boolean, and Content entity reference. A hand-written component.yml may still declare type: array over a Formatted text or Boolean item shape, which Canvas validates and stores; the editor then offers no checkbox for it.

Constraints once a component is in the library:

  • A prop’s name and type cannot change, and a single-value prop cannot become multi-value; delete and recreate the prop instead.
  • Adding a required prop (or making an optional prop required) needs a default or example value; existing instances are auto-filled with it.
  • Changed default/example values are not backfilled to instances already saved on pages.
  • Prop names are converted to camelCase (Heading TextheadingText).

What declares each editor type, written exactly as the editor’s Component Data pane writes it into the schema:

Editor type Declaration
Text type: string
Formatted text type: string plus contentMediaType: text/html (with x-formatting-context: block, the only supported context)
Link type: string plus format: uri-reference (relative or absolute), or format: uri (absolute only)
Image type: object plus $ref: json-schema-definitions://canvas.module/image (an object of src (required), alt, width, height)
Video type: object plus $ref: json-schema-definitions://canvas.module/video (src required, optional poster)
Date and time type: string plus format: date (date only) or format: date-time (date and time)
Integer type: integer
Number type: number
Boolean type: boolean
List: text type: string plus enum listing the allowed values, with a meta:enum label per value
List: integer type: integer plus enum and meta:enum
Content entity reference type: object plus $ref: json-schema-definitions://canvas.module/content-entity-reference; declare it through the editor’s Component Data pane, which also records the entity type and bundle it accepts. It cannot be listed under required and takes no examples

Three rules sit across every row:

  • Multi-value wraps the declaration. type: array with the single-value declaration moved inside items; an optional maxItems caps the count and must be at least 2 (a single-value prop is a non-array type, never maxItems: 1). A required multi-value prop also carries minItems: 1, which the platform writes for you.
  • examples[0] is the default value the editor prefills; a JSON Schema default: key is discarded. A prop listed under required must carry an example value, or the component fails validation.
  • An unrecognized declaration disables the whole component, not just the prop. The error names both: Drupal Canvas does not know of a field type/widget to allow populating the <prop> prop, with the shape <schema>. Everything in the table above maps; a declaration outside it (an arbitrary type: object without a $ref, an unsupported format) does not.

Worked YAML for each type is in Canvas’s own reference, component metadata. An SDC’s *.component.yml on Cloud Platform accepts the same declarations (under props.properties, with required inside props rather than top-level) plus wider JSON Schema (pattern, maxLength, minimum/maximum); its worked examples are on the SDC props page.

Field Type Use
title string Label in the editor’s Slots tab
description string Help text in the editor
examples array Example HTML/JSX that determines the slot’s default preview rendering

The component receives the slot as an argument named after the slot key and renders it as children. A slot’s name cannot be renamed once the component is in the library: renaming acts as delete-plus-add and hides child components placed in the original slot (re-adding a slot with the exact original name before saving restores them).

name: My Hero
machineName: my-hero
status: false
required: []
props:
properties:
greeting:
type: string
title: Greeting
description: The greeting to display
examples:
- My Hero
ctaLink:
type: string
format: uri
title: CTA Link
examples:
- https://example.com
slots:
content:
title: Content
description: Content to display below the greeting
examples:
- <div>Example slot content</div>
  • Canvas quickstart: scaffold, validate, push, and the client-credentials setup.
  • Push to a live site: push’s plan and confirmation step, pull --skip-overwrite, and what the site does with a planned delete.
  • Canvas components section: validate, build, and the prop/slot schema.
  • GitHub Sync: the branch-namespaced CANVAS_* credentials the sync workflows consume.

Was this page helpful?