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.
Installation
Section titled “Installation”# per project (the Nebula scaffold includes it)npm install @drupal-canvas/cli# globalnpm install @drupal-canvas/cli -gProjects scaffolded with npx @drupal-canvas/create@latest have it preinstalled; run it as npx canvas <command>.
Authentication
Section titled “Authentication”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 Credentialscheckbox and the scopes below, supplied via environment variables. This is a distinct OAuth client from theDRUPAL_*credentials used for the Content API and MCP server; the CLI reads its ownCANVAS_*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).
OAuth scopes
Section titled “OAuth scopes”| 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.
Configuration
Section titled “Configuration”Environment variables (.env)
Section titled “Environment variables (.env)”| 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).
canvas.config.json (optional, committed)
Section titled “canvas.config.json (optional, committed)”| 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 |
Commands
Section titled “Commands”canvas login
Section titled “canvas login”Log in to a Canvas site via browser (OAuth 2.0 authorization code + PKCE); stores tokens in ~/.config/drupal-canvas/oauth.json.
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 failedSee the authentication caveat before relying on browser-login tokens for pull/push on a Source CMS site.
canvas logout
Section titled “canvas logout”Remove stored credentials for a site from ~/.config/drupal-canvas/oauth.json.
npx canvas logout [options]| Flag | Description |
|---|---|
--site-url <url> |
Site to log out of (prompted if omitted) |
Example:
npx canvas logout --site-url https://example.com
┌ Drupal Canvas logout│● Credentials removed: https://example.com│└ Logout completedcanvas pull
Section titled “canvas pull”Pull components, global CSS, and optional pages, content templates, global regions, and Brand Kit fonts from Drupal to the local filesystem.
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 completedcanvas push
Section titled “canvas push”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).
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 completedTwo 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 deniedandNot foundpages withUnable 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 failedAnother 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 failedcanvas build
Section titled “canvas build”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.
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:
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 completedcanvas validate
Section titled “canvas validate”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.
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 failedcanvas scaffold
Section titled “canvas scaffold”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.
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 completedNote: 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).
canvas agents-context
Section titled “canvas agents-context”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).
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 pulledcanvas reconcile-media
Section titled “canvas reconcile-media”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.
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 skippedcanvas download / canvas upload (removed)
Section titled “canvas download / canvas upload (removed)”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 removedcanvas upload prints the same for canvas push.
Component schema
Section titled “Component schema”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 pullwrites TypeScript components asindex.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.
Top-level fields (component.yml)
Section titled “Top-level fields (component.yml)”| 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 Text →
headingText).
Prop type declarations
Section titled “Prop type declarations”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: arraywith the single-value declaration moved insideitems; an optionalmaxItemscaps the count and must be at least 2 (a single-value prop is a non-array type, nevermaxItems: 1). A required multi-value prop also carriesminItems: 1, which the platform writes for you. examples[0]is the default value the editor prefills; a JSON Schemadefault:key is discarded. A prop listed underrequiredmust 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 arbitrarytype: objectwithout a$ref, an unsupportedformat) 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.
Slot definitions (slots.<slotName>)
Section titled “Slot definitions (slots.<slotName>)”| 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).
Example
Section titled “Example”name: My HeromachineName: my-herostatus: falserequired: []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.comslots: content: title: Content description: Content to display below the greeting examples: - <div>Example slot content</div>Used in
Section titled “Used in”- 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?
What went wrong?
Still stuck? Contact Acquia Support (opens in a new tab)