Playground and Blueprints

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages.

WordPress Playground runs a complete WordPress site without a web server or a database server: PHP is compiled to WebAssembly and the database is SQLite. A Blueprint, a JSON file, describes what such a site contains. The same Blueprint drives the site in a browser tab, in an embedded <iframe>, in the Playground CLI on a developer machine or in CI, and behind the Preview buttons of the plugin directory and of GitHub pull requests. Plugin authors use it for demos and reproducible bug reports, theme designers for disposable design sites, reviewers for one-click previews, and teachers for workshops where nobody installs anything.

The daily local use of Playground (the browser site, start and server, migrating from wp-now) is introduced in Local Development Environments. This page covers the rest: how it works, the full Blueprint format, the Query and JavaScript APIs, the CLI commands beyond start, plugin and pull request previews, and the limits.

The page was written against WordPress 7.1.3 (examples tested; 7.1.4, released on 2026-10-28, is the current security release and the one to run in production), PHP 7.4 minimum and 8.3+ recommended, and the Playground CLI @wp-playground/cli and client @wp-playground/client 3.1.61, and checked on 2026-10-28. WordPress 7.2 is in beta and due in December 2026. Every command below ran with the CLI on Node.js 24.

How Playground works

Part What it does

PHP.wasm

PHP 7.4 to 8.5 compiled to WebAssembly. Each version is a separate .wasm file, so switching version means downloading another binary. The browser download is 15 to 30 MB.

Virtual file system

WordPress lives under /wordpress in an in-memory file system. The CLI can mount host folders into it; the browser keeps saved sites in browser storage (OPFS) or a chosen local folder.

SQLite

The SQLite Database Integration plugin is loaded as a drop-in and translates every MySQL query into SQLite. The database file is /wordpress/wp-content/database/.ht.<random>/.ht.sqlite (the FQDB constant).

Request routing

In the browser a service worker intercepts the requests of the <iframe> and hands them to PHP running in a web worker; in Node.js the CLI starts an HTTP server in front of the same runtime.

Networking

WebAssembly can only call JavaScript. In the browser wp_safe_remote_get() and similar calls become fetch() requests (subject to CORS); in Node.js a WebSocket-to-TCP proxy lets PHP open sockets.

The ways to run it:

Runtime Entry point Typical use

Browser app

https://playground.wordpress.net/ plus Query API parameters or a Blueprint URL

Trying a plugin or theme, sharing a reproducible set-up as a link, previews

Embedded

@wp-playground/client and startPlaygroundWeb() in an own page

Interactive documentation, tutorials, product demos

Playground CLI

npx @wp-playground/cli@3.1.61 with start, server, run-blueprint, build-snapshot, php

Local development without Docker, smoke tests and snapshots in CI

wp-env

wp-env start --runtime=playground (experimental in wp-env 12)

Projects already on wp-env that cannot run Docker

Blueprints

A Blueprint is a JSON object with a few top-level properties and an ordered list of steps. Pointing $schema at https://playground.wordpress.net/blueprint-schema.json gives editors completion and validation.

Property Meaning

$schema

JSON Schema for the Blueprint format; ignored at run time.

meta

title, description, author (a GitHub user name) and categories, used by Blueprint galleries.

landingPage

Relative URL opened once the steps have run, for example /wp-admin/site-editor.php. Defaults to the admin.

preferredVersions

php (7.4 to 8.5, latest; next in the browser only, no minor versions such as 7.4.1) and wp (one of the last seven major versions, an exact release such as 7.1.3, latest, beta, nightly/trunk, or false for PHP without WordPress). Only these two keys belong here.

features

networking (default true; with false every outgoing HTTP call fails at once, so plugins cannot be installed from the admin) and intl (load the dynamic intl extension).

extraLibraries

["wp-cli"] preloads WP-CLI; it is added automatically when a wp-cli step is present.

constants

Shorthand for defineWpConfigConsts: constants defined on every request.

plugins

Shorthand: directory slugs or resources to install and activate, for example ["query-monitor"].

siteOptions

Shorthand for setSiteOptions.

login

Shorthand: true logs in as admin/password, or an object with username and password.

steps

Ordered list of step objects, each with a step name and its own arguments.

phpExtensionBundles

Deprecated and ignored; the schema says it can be removed. Older Blueprints, including the plugin directory’s examples, still carry "kitchen-sink".

Shorthands are expanded before the steps array, so plugins, login, siteOptions and constants always run first. When order matters (a constant that must exist before a plugin is activated, a login as a user created by a step), write explicit steps instead. A step number in an error message counts the expanded list.

In preferredVersions.wp, 7.1 means the newest 7.1 point release: on the check date it resolved to 7.1.4. A Blueprint that must reproduce a bug in one release names it exactly: "wp": "7.1.3" booted 7.1.3.

Steps

Step Arguments and remarks

installPlugin

pluginData (a resource: zip, single PHP file or directory) and options (activate, targetFolderName, onError: skip-plugin|throw, activationOptions), ifAlreadyInstalled. pluginZipFile is the deprecated older name.

installTheme

themeData and options (activate, importStarterContent, targetFolderName). themeZipFile is deprecated.

activatePlugin

pluginPath, the main file relative to wp-content/plugins or absolute, and an optional pluginName for messages.

activateTheme

themeFolderName.

login

username (default admin) and password. It sets PLAYGROUND_AUTO_LOGIN_AS_USER, read by an internal must-use plugin on the first request.

setSiteOptions

options: a map passed to update_option() one by one.

updateUserMeta

userId and meta.

defineWpConfigConsts

consts, written to wp-config.php; the right place for WP_DEBUG while debugging a Blueprint.

runPHP

code: a PHP script. WordPress is not loaded: start with require_once '/wordpress/wp-load.php'; or get "Undefined constant ABSPATH".

runPHPWithOptions

options: code plus request data such as body and headers.

wp-cli

command: a WP-CLI command line, as a string or an array of arguments. Not every command works and there is no shell ($(...) is not expanded).

runSql

sql: a resource with one statement per line, executed through $wpdb on SQLite.

importWxr

file: a WXR resource, imported with the WordPress Importer; fetchAttachments, importUsers, authorsMap, rewriteUrls and urlMapping control the import.

importWordPressFiles

wordPressFilesZip: a whole site exported from Playground.

importThemeStarterContent

Imports the active theme’s starter content.

setSiteLanguage

language, for example es_ES; downloads translations, so it needs networking.

enableMultisite

Converts the site into a network.

writeFile, writeFiles

path and data (a string or a resource); writeToPath and filesTree for a directory.

mkdir, cp, mv, rm, rmdir, unzip

File operations inside the virtual file system.

defineSiteUrl, resetData

siteUrl sets WP_HOME and WP_SITEURL; resetData deletes posts, pages, custom post types and comments (or the contentTypes given).

Resources

Steps that need a file take a resource reference rather than a path:

resource Fields Use

url

url

A built zip on a public URL that downloads without cookies or login and sends CORS headers (a release asset, an S3 object). An HTML error page or a login page fails as "not a valid zip".

wordpress.org/plugins, wordpress.org/themes

slug, optional version

Directory plugins and themes. The slug is separate: "wordpress.org/plugins/akismet" is wrong.

git:directory

url, ref, refType (branch, tag, commit), path

Source straight from a repository (sparse checkout through a CORS proxy). A branch or tag needs refType; only HEAD resolves reliably without it.

vfs

path

A file already in the virtual file system, for example in a folder mounted by the CLI.

literal

name, contents

Inline file content.

bundled

path

A file shipped next to blueprint.json in a Blueprint bundle.

The Lumen Press demo Blueprint

Lumen Press keeps a Blueprint that builds a demo of the catalogue: the lumen theme and the lumen-catalog plugin from locally built zips, a site name, pretty permalinks and three seeded books. Both zips have a root folder named after the slug (lumen-catalog/lumen-catalog.php, lumen/style.css), and the plugin zip includes vendor/ from Composer, because Playground installs exactly what is in the zip and runs no build. The release zip is produced as described in Publishing to the Plugin Directory; the steps below assume both zips are in a dist/ folder.

{
	"$schema": "https://playground.wordpress.net/blueprint-schema.json",
	"meta": {
		"title": "Lumen Press catalogue demo",
		"author": "lumenpress"
	},
	"landingPage": "/wp-admin/edit.php?post_type=lp_book",
	"preferredVersions": { "php": "8.4", "wp": "7.1" },
	"features": { "networking": false },
	"login": true,
	"steps": [
		{
			"step": "installTheme",
			"themeData": { "resource": "vfs", "path": "/lumen-dist/lumen.zip" },
			"options": { "activate": true }
		},
		{
			"step": "installPlugin",
			"pluginData": { "resource": "vfs", "path": "/lumen-dist/lumen-catalog.zip" },
			"options": { "activate": true }
		},
		{
			"step": "setSiteOptions",
			"options": {
				"blogname": "Lumen Press (Playground)",
				"permalink_structure": "/%postname%/"
			}
		},
		{
			"step": "wp-cli",
			"command": "wp post create --post_type=lp_book --post_status=publish --post_title='The Lighthouse Ledger' --meta_input='{\"lp_isbn\":\"978-1-23456-789-7\"}'"
		},
		{
			"step": "runPHP",
			"code": "<?php require_once '/wordpress/wp-load.php'; foreach ( array( 'The Glass Orchard' => '978-1-23456-790-3', 'The Quiet Lighthouse' => '978-1-23456-791-0' ) as $title => $isbn ) { wp_insert_post( array( 'post_type' => 'lp_book', 'post_status' => 'publish', 'post_title' => $title, 'meta_input' => array( 'lp_isbn' => $isbn ) ) ); }"
		}
	]
}

The two seeding styles are deliberate: a wp-cli step suits one simple command, a runPHP step suits anything with logic. "networking": false keeps the demo from calling out (no update checks, no distributor API), which also makes it start faster. lp_isbn must be registered with show_in_rest for it to appear in REST responses, and lp_book needs 'show_in_rest' => true for the /wp/v2/lp_book route to exist (see Custom Post Types and Taxonomies).

The CLI mounts dist/ at /lumen-dist, which is where the vfs resources look:

npx @wp-playground/cli@3.1.61 server \
  --port=9400 --php=8.4 --wp=7.1 \
  --mount=./dist:/lumen-dist \
  --blueprint=./blueprint.json
WordPress Playground CLI

PHP 8.4  WordPress 7.1
Extensions intl, redis, memcached
Mount ./dist → /lumen-dist
Blueprint ./blueprint.json

Ready! WordPress is running on http://127.0.0.1:9400 (6 workers)

The server answers the first request of a new client with a 302 to the same URL; with "login": true that response also sets the admin cookies and playground_auto_login_already_happened=1. A browser follows it transparently, while plain curl gets an empty body. Sending that cookie, or -L with a cookie jar (-L -c jar -b jar), avoids the problem:

curl -s -b 'playground_auto_login_already_happened=1' \
  'http://127.0.0.1:9400/wp-json/wp/v2/lp_book?_fields=id,title.rendered,meta.lp_isbn,link&orderby=title&order=asc'
[
  { "id": 5, "link": "http://127.0.0.1:9400/books/the-glass-orchard/", "title": { "rendered": "The Glass Orchard" }, "meta": { "lp_isbn": "978-1-23456-790-3" } },
  { "id": 4, "link": "http://127.0.0.1:9400/books/the-lighthouse-ledger/", "title": { "rendered": "The Lighthouse Ledger" }, "meta": { "lp_isbn": "978-1-23456-789-7" } },
  { "id": 6, "link": "http://127.0.0.1:9400/books/the-quiet-lighthouse/", "title": { "rendered": "The Quiet Lighthouse" }, "meta": { "lp_isbn": "978-1-23456-791-0" } }
]

The front page reported WordPress 7.1.4 in its generator tag, /books/ answered 200 and the theme’s assets came from themes/lumen/. Because only dist/ is mounted, the site itself lives in a temporary directory and is gone when the server stops; that is what a demo or a test wants.

Blueprint bundles

A bundle is a blueprint.json plus the files it references, as a directory, a zip (with blueprint.json at the root or one folder deep, so a macOS "Compress" archive works), a folder in a Git repository, or a remote URL. Steps reference the files with { "resource": "bundled", "path": "/lumen-catalog.zip" }. The browser app opens a zipped bundle with ?blueprint-url=https://…/bundle.zip. The CLI refuses to read files next to a local Blueprint unless told to:

# bundle/ holds blueprint.json, lumen.zip and lumen-catalog.zip
npx @wp-playground/cli@3.1.61 run-blueprint --php=8.4 --wp=7.1 \
  --blueprint=./bundle/blueprint.json --blueprint-may-read-adjacent-files

Without the flag the run stopped with "Playground restricts access to local resources by default as a security measure"; with it, it finished with exit code 0.

Errors and pitfalls

Symptom Cause and fix

InvalidBlueprintError

A valid property in the wrong object: activate belongs in options, not in pluginData; plugins is top level, not inside preferredVersions; pluginData and pluginZipFile mixed in one step.

ResourceDownloadError, "neither a valid JSON nor a ZIP file"

The URL returned an HTML page (a GitHub file viewer, a login page, an expired CI artifact) or lacks CORS headers.

PHP.run() failed with exit code 255

A PHP fatal error, often activation of a plugin whose vendor/ was not in the zip. Add { "step": "defineWpConfigConsts", "consts": { "WP_DEBUG": true } } early and read the first "Fatal error" line.

"Undefined constant ABSPATH"

A runPHP step without require_once '/wordpress/wp-load.php';.

"Current PHP version (…) does not meet minimum requirements"

preferredVersions older than the plugin’s Requires PHP/Requires at least; for lumen-catalog that is PHP 7.4 and WordPress 6.9.

Plugin missing after install

The zip has files at its root instead of in a lumen-catalog/ folder.

A git:directory branch resolves to HEAD

refType is missing.

WP-CLI "Error establishing a database connection" with --mount-before-install

The SQLite integration is loaded from Playground’s internal files, not from the mounted site; install it into the mounted wp-content as the troubleshooting guide shows.

The Query API

The browser app reads its configuration from the URL, which makes a Playground a link:

Parameter Default Meaning

blueprint-url

URL of a Blueprint JSON file or bundle zip (CORS required).

plugin, theme

Directory slug to install; repeat for several. Installing logs the user in.

php, wp

8.5, latest

Versions, with the same values as preferredVersions.

url

/wp-admin/

First page to open.

mode

browser-full-screen

seamless hides the Playground browser chrome, for embedding.

login

yes

Log in as the administrator.

networking

yes

no disables outgoing HTTP.

multisite

no

yes starts a network.

import-site, import-wxr

URL of a site zip or a WXR file to import.

language

en_US

Locale; needs networking for the translations.

storage, can-save, site-slug

storage=temp for a site discarded on reload; can-save=no hides saving; site-slug selects a saved site.

core-pr, gutenberg-pr, gutenberg-branch

Run a pull request or branch of wordpress-develop or Gutenberg.

lazy

Do not boot until the visitor clicks Run.

A link to the hosted demo Blueprint, and an embed of a directory plugin in the Site Editor:

<a href="https://playground.wordpress.net/?blueprint-url=https://downloads.lumenpress.example/playground/bundle.zip">
	Try the Lumen Press catalogue
</a>

<iframe
	src="https://playground.wordpress.net/?plugin=query-monitor&url=/wp-admin/site-editor.php&mode=seamless&storage=temp"
	title="WordPress Playground" width="1000" height="640" loading="lazy"></iframe>

downloads.lumenpress.example must send Access-Control-Allow-Origin (at least for https://playground.wordpress.net) and serve the files without authentication. Query parameters are visible to anyone with the link and end up in server logs: they must never carry a password, an application password or an API key.

The JavaScript API

@wp-playground/client controls a Playground running in an <iframe> of an own page. startPlaygroundWeb() loads remote.html (the bare runtime, without the browser chrome of index.html) into the frame, runs the Blueprint and returns a client. A JSON Blueprint is accepted only at start; afterwards the client’s methods or the Blueprint functions (installPlugin( client, ... ), login( client, ... )) change the running site.

<iframe id="lumen-demo" title="Lumen Press demo" style="width: 100%; height: 640px; border: 0"></iframe>
<script type="module">
	import { startPlaygroundWeb } from 'https://unpkg.com/@wp-playground/client@3.1.61/index.js';

	const client = await startPlaygroundWeb( {
		iframe: document.getElementById( 'lumen-demo' ),
		remoteUrl: 'https://playground.wordpress.net/remote.html',
		blueprint: {
			preferredVersions: { php: '8.4', wp: '7.1' },
			login: true,
			steps: [
				{
					step: 'installPlugin',
					pluginData: { resource: 'url', url: 'https://downloads.lumenpress.example/playground/lumen-catalog.zip' },
					options: { activate: true },
				},
			],
		},
	} );
	await client.isReady();

	const result = await client.run( {
		code: "<?php require '/wordpress/wp-load.php'; echo (int) wp_count_posts( 'lp_book' )->publish;",
	} );
	console.log( 'Published books:', result.text );
	await client.goTo( '/books/' );
</script>
Method Purpose

isReady()

Resolves when WordPress has booted and the Blueprint has run.

run( { code } ), run( { scriptPath } )

Runs PHP once (snippet or file mode), resetting PHP state each time; returns text, bytes, errors, exitCode.

request( { url, method, headers, body } )

Sends an HTTP request through WordPress, as a browser would.

goTo( path ), getCurrentURL()

Navigates the frame.

writeFile(), readFileAsText(), mkdirTree(), listFiles(), unlink()

Virtual file system.

setPhpIniEntries( client, { ... } )

Changes php.ini values, such as display_errors.

From npm the same function is import { startPlaygroundWeb } from '@wp-playground/client' after npm install @wp-playground/client@3.1.61. While debugging, both index.html and remote.html expose the client as window.playground in the browser console. Opting in to workerMode: 'shared' (with a scope) shares one runtime between tabs, but requires serving remote.html from the own origin.

The Playground CLI

@wp-playground/cli needs Node.js 20.18 or later and no Docker. Its commands:

Command Use

start

Detects whether the current directory is a plugin, theme, wp-content or full site, mounts it, logs in, opens a browser and keeps the site in ~/.wordpress-playground/sites/<hash>/; --reset starts over. Covered in Local Development Environments.

server

Everything explicit: mounts, Blueprint, versions, port. Without a mount of /wordpress or wp-content the site lives in a temporary directory removed when the server stops (and swept after two days).

run-blueprint

Applies a Blueprint and exits without serving, which turns a Blueprint into a smoke test: a failing step exits non-zero.

build-snapshot

Applies a Blueprint and writes the resulting site as a zip, importable with import-site or importWordPressFiles.

php

Runs a PHP script with Playground’s PHP.

Option (for server) Meaning

--php=8.4, --wp=7.1

Versions; PHP defaults to 8.3, WordPress to latest.

--port=9400, --site-url=URL

Listening port (default 9400) and the URL WordPress uses for links.

--mount=host:vfs

Mount a host folder after installation; repeatable. --mount-dir host vfs is the Windows-friendly form.

--mount-before-install=host:vfs

Mount before installation, for example a full checkout at /wordpress, with --wordpress-install-mode=install-from-existing-files-if-needed to keep an existing site.

--auto-mount[=path]

The start detection, for server.

--blueprint=path-or-url

A Blueprint JSON file, bundle directory, bundle zip or URL.

--blueprint-may-read-adjacent-files

Consent for bundled resources of a local Blueprint.

--login

Log in as the administrator (default false for server, true for start).

--skip-sqlite-setup

Do not install the SQLite integration, for a site that connects to MySQL itself.

--xdebug, --experimental-unsafe-ide-integration=vscode

Xdebug, and its VS Code or PhpStorm configuration.

--phpmyadmin

Installs phpMyAdmin over the SQLite database.

--workers=n

Request workers; defaults to min(6, CPUs − 1). Parallel end-to-end suites may need more.

--follow-symlinks

Follow symlinks out of mounted folders; this exposes files outside the mount.

--verbosity=debug, --debug

Show the temporary directory and the PHP error log on boot failures.

npx @wp-playground/cli@3.1.61 --help and server --help list the full set. The CLI logs every visitor in as the administrator when login is on and serves http://127.0.0.1:<port> with debugging-friendly defaults; it is not a way to publish a site. The npm package @wp-now/wp-now is deprecated and its npm entry points to the Playground CLI; wp-now start maps to start with the same --wp, --php, --blueprint, --skip-browser and --reset options.

Plugin previews on WordPress.org

The plugin directory shows a Preview button next to Download when two conditions hold: a valid assets/blueprints/blueprint.json is committed to the plugin’s SVN assets folder, and a committer has set the preview to public on the plugin’s Advanced tab. With only the Blueprint present, committers see a Test Preview button to check the result first. A plugin without a Blueprint gets a notice with a Test link that uses a generated Blueprint and a link to download that file as a starting point. Only one Blueprint per plugin is supported.

The directory edition of lumen-catalog (without Update URI, see Publishing to the Plugin Directory) keeps its preview Blueprint in .wordpress-org/blueprints/blueprint.json, which the deploy action copies to assets/:

{
	"$schema": "https://playground.wordpress.net/blueprint-schema.json",
	"landingPage": "/books/",
	"preferredVersions": { "php": "8.3", "wp": "latest" },
	"steps": [
		{ "step": "login", "username": "admin", "password": "password" },
		{
			"step": "installPlugin",
			"pluginData": { "resource": "wordpress.org/plugins", "slug": "lumen-catalog" },
			"options": { "activate": true }
		},
		{ "step": "setSiteOptions", "options": { "permalink_structure": "/%postname%/" } },
		{
			"step": "runPHP",
			"code": "<?php require_once '/wordpress/wp-load.php'; foreach ( array( 'The Lighthouse Ledger' => '978-1-23456-789-7', 'The Glass Orchard' => '978-1-23456-790-3' ) as $title => $isbn ) { wp_insert_post( array( 'post_type' => 'lp_book', 'post_status' => 'publish', 'post_title' => $title, 'meta_input' => array( 'lp_isbn' => $isbn ) ) ); }"
		}
	]
}

The handbook’s own examples still use pluginZipFile, themeZipFile and phpExtensionBundles; current Playground accepts the first two as deprecated aliases and ignores the third, so new Blueprints use pluginData, themeData and nothing else. A preview is a disposable site: what a visitor does there never reaches the plugin author, and demo content should be fictitious. Seeding through runPHP rather than importWxr keeps the demo free of a separately hosted WXR file.

Pull request previews in GitHub Actions

WordPress/action-wp-playground-pr-preview adds a Preview button to the description (or a comment) of each pull request, opening Playground with the plugin or theme from that pull request. Two set-ups exist:

  • No build step: one workflow on pull_request with plugin-path: . (or theme-path); Playground fetches the branch through git:directory. Only valid when the committed files are exactly what WordPress runs.

  • With a build step: a reusable preview-build.yml workflow runs the build with read-only permissions and uploads zips; a preview-publish.yml workflow, started by workflow_run, publishes them as assets of a public ci-artifacts prerelease and writes the button. This is also the safe set-up for pull requests from forks.

lumen-catalog needs composer install (for vendor/) and wp-scripts build (for build/blocks/), so it uses the second set-up. .github/workflows/pr-preview-build.yml:

name: PR Preview - Build
on:
  pull_request:
    types: [opened, synchronize, reopened, edited]
jobs:
  build:
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-build.yml@v4.0.0
    with:
      artifacts: lumen-catalog=lumen-catalog.zip
      node-version: '24'
      php-version: '8.3'
      build-command: |
        set -euo pipefail
        composer install --no-dev --optimize-autoloader --no-interaction
        npm ci
        npm run build
        npm run plugin-zip

npm run plugin-zip writes lumen-catalog.zip with a lumen-catalog/ root folder, the shape Playground expects. .github/workflows/pr-preview-publish.yml:

name: PR Preview - Publish
on:
  workflow_run:
    workflows: ['PR Preview - Build']
    types: [completed]
permissions:
  contents: write
  pull-requests: write
jobs:
  publish:
    permissions:
      contents: write
      pull-requests: write
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-publish.yml@v4.0.0
    with:
      blueprint: |
        {
          "$schema": "https://playground.wordpress.net/blueprint-schema.json",
          "landingPage": "/wp-admin/edit.php?post_type=lp_book",
          "preferredVersions": { "php": "8.4", "wp": "7.1" },
          "steps": [
            { "step": "login", "username": "admin", "password": "password" },
            {
              "step": "installPlugin",
              "pluginData": { "resource": "url", "url": "{{ARTIFACT_URL:lumen-catalog}}" },
              "options": { "activate": true }
            }
          ]
        }

{{ARTIFACT_URL:lumen-catalog}} is replaced by the public URL of the uploaded zip; the name matches the left side of artifacts. The workflows must exist on the default branch before they run for pull requests, and both must use the same action version. Rules that matter for security:

  • Never switch to pull_request_target to get write permissions: it would run untrusted pull request code with them. The two-workflow split exists precisely so that the code runs read-only and only the trusted publish workflow writes.

  • The zips become public release assets. A private repository’s code would be published that way, and Playground cannot fetch private URLs anyway; private repositories need another preview host.

  • secrets.GITHUB_TOKEN is provided by GitHub; no secret is created, and nothing secret goes into the Blueprint.

A preview that opens but shows "PHP.run() failed with exit code 255" usually means a missing vendor/, the case the build step is there for. Previews complement, not replace, the test jobs in Testing and the pipeline in Deployment and CI/CD.

Limitations

Area Limitation

Database

SQLite through a translation layer, not MySQL: plugins that send unusual SQL, use MySQL-only functions or connect to MySQL themselves may fail. Version 2.0 of the integration passes about 99% of the WordPress unit tests; a passing Playground run is not proof of MySQL compatibility. Production remains MySQL or MariaDB (see Requirements and Hosting).

Network

Networking is on by default in current versions, but in the browser every request is a fetch(): targets without CORS headers fail, and raw sockets (SMTP, Redis, a remote MySQL) are unavailable. Blueprints that must not call out set "networking": false.

Persistence

Browser sites are autosaved, at most five, and autosaves are recovery points: clearing site data or private browsing loses them. storage=temp sites vanish on reload. server without a mount loses everything on exit. Export a zip to keep a site.

WP-CLI

The wp-cli step and the CLI support most commands but not all, and offer no shell features.

Performance

5 to 10 s for a bare site, 30 to 60 s with a large plugin, slower on phones. Not a load-testing target.

Extensions

A fixed set of PHP extensions; others need a PHP.wasm extension manifest (php-extension).

Frame

The site runs in an <iframe>: target="_top" links reload the host page and some pop-ups do not show.

Security model

Everyone is logged in as an administrator of a throwaway site. Never put real user data, production dumps or credentials into a Blueprint or a public preview.

Corrections and complements to older books

None of the three books in the bibliography covers Playground. Ultimate WordPress Handbook and All-in-One For Dummies set up local sites with desktop stacks (Local, XAMPP, MAMP), which still work but leave nothing reproducible to share: a Blueprint in the repository gives every reviewer, tester and student the same site with one link or one command. Pro WordPress registers custom post types without show_in_rest; such a type has no /wp/v2/ route, so the REST check used above, the block editor and every headless use of Playground fail for it. Tutorials that still use wp-now, pluginZipFile or phpExtensionBundles predate the current CLI and schema; the replacements are listed above and in Where Older Books and Tutorials Are Outdated.