Repository navigation
Echo Actions And Compare
An action is something that makes your site send messages: a form post, a WP-CLI or artisan command, a replayed webhook. This page covers the actions you list in .echo/config.yaml and how each one runs, how messages are tied to the action that caused them and snapshots. It also covers how compare matches messages, what it ignores, what each class of change means and how to accept a change you meant.
rhythm echo snapshot --name before-update # run every action, save what each sent
rhythm echo compare # run them again, show what changed
rhythm echo run # run them once, just check the messagesAll three need Echo running (rhythm echo up) and a config with at least one action, found in the folder you're in or one above it (see Configuration). Forms in a real browser and multi-step journeys are better run by Soundcheck, which uses the same comparison; see With Soundcheck.
# .echo/config.yaml
site: https://staging.shop.test
flush: ["wp cron event run --due-now", "wp action-scheduler run"]
actions:
- name: Contact form
http: {method: POST, url: /wp-admin/admin-ajax.php, form: {action: contact, email: "${fake:email}", message: Hello}}
expect: {email: 2}
- name: Completed order
command: wp eval 'do_action("woocommerce_order_status_completed", 123);'
expect: {email: 1, analytics: {purchase: 1}}
ignore: ["line_items[].meta_data", "x-request-id"]
- name: Stripe payment succeeded
replay: {event: payment_intent.succeeded, to: /?wc-api=wc_stripe, secret_env: ECHO_STRIPE_WEBHOOK_SECRET}
parallel: falseEach action has a name and exactly one of http, command or replay. Mistakes stop the command before anything runs, with exit 2 and the line they're on:
rhythm echo: /tmp/t2/.echo/config.yaml: line 2: action "A": set exactly one of http, command or replay
rhythm echo: /tmp/t2/.echo/config.yaml: line 4: action "contact-form" and action "Contact Form" (line 2) both get the ID action:contact-form; rename one
rhythm echo: /tmp/t2/.echo/config.yaml: line 2: action "!!!": needs a letter or digit from a to z or 0 to 9 in its name, so compare can tell it apart; add one, like "!!! 1"
rhythm echo: /tmp/t3/.echo/config.yaml: yaml: line 4: mapping values are not allowed in this context
Actions are matched by an ID made from the name (see Run IDs), so two names that give the same ID are refused. A name needs at least one letter or digit from a to z or 0 to 9: 注文 is refused, 注文 checkout works. The last error is YAML, not Echo: a value containing : (a command with Subject: …, a PHP array) must be quoted, or written as a block with command: | or command: >-.
| Key | Description |
|---|---|
name |
Required, unique. |
http, command, replay
|
What the action does; one of them. |
env |
Extra environment variables for a command. |
expect |
The least it must send; see expect. |
ignore |
What compare leaves out for this action; see ignore. |
quiet |
This action's own quiet window; see Quiet and timeout. |
parallel |
false runs it alone, never beside another action; see Parallel runs. |
| Key | Default | Description |
|---|---|---|
url |
(required) | Absolute, or relative to site:. |
method |
GET, or POST when there's a form, json or body
|
|
form |
Fields, sent as application/x-www-form-urlencoded. |
|
json |
Any YAML value, sent as application/json. Placeholders in its strings are filled before it's encoded, so quotes and backslashes in a value stay valid JSON. |
|
body |
A raw body. | |
headers |
Extra headers. |
The request carries the run ID as a header and a cookie, doesn't follow redirects and times out after timeout. An answer of 400 or more fails the action (POST https://staging.shop.test/contact returned 500 Internal Server Error), so a form that rejects the test data doesn't pass as "sent nothing". Redirects (3xx) are fine. A URL under one of the config's production: entries is refused before anything is sent; see Production. Errors show the URL with its secrets masked. Every value an ${env:NAME} put into the request shows as •••••.
- name: Weekly digest
command: wp cron event run shop_weekly_digest
env: {WP_CLI_STRICT_ARGS_MODE: "1"}The command runs with sh -c (cmd /C on Windows, so write %VAR% there, not $VAR) in the project's folder, the one holding .echo/, wherever you run Echo from. RHYTHM_ECHO_RUN is set to the run ID and the action's env: added. Connectors in CLI processes (WP-CLI, artisan, Node scripts) read RHYTHM_ECHO_RUN, so what they send is tied to the action; a script can forward it to the site as X-Echo-Run. A non-zero exit fails the action, with the first 300 characters of stderr in the error. Ctrl+C stops the command's whole process tree at once. A command that leaves a background process holding its output fails with exec: WaitDelay expired before I/O complete: send the background job's output to /dev/null. For a site in Docker, pass the variable through:
command: docker exec -e RHYTHM_ECHO_RUN shop-cli wp eval 'wp_remote_post("https://hooks.slack.com/services/…", array("body" => "{}"));'| Key | Description |
|---|---|
event |
A library event (payment_intent.succeeded, stripe/invoice.paid) or a path to a JSON file, relative to the project folder. |
to |
Where to send it: absolute, or relative to site:. |
set |
Field overrides, data.object.amount: 1999, applied in key order. |
secret_env |
The variable holding the signing secret; default per scheme, such as ECHO_STRIPE_WEBHOOK_SECRET. |
Replays follow the same safety rules as rhythm echo replay: sealed Echo, staging or local targets that aren't under production:, test secrets only. With Echo observing, the action fails before anything is sent: Echo is in observe mode; replays only run sealed, so the site's reactions can't reach anyone (rhythm echo up --seal). A non-2xx answer fails the action. See Replay.
expect: {email: 2, sms: 1, analytics: {purchase: 1}}The least the action must send, per channel (email, sms, whatsapp, chat, webhook, analytics, payment, push, http), as a count or as counts per event. When it falls short, the action gets the error expect/unmet, one per channel or event short of its count, which is BROKE in a comparison:
[BROKE] Sent fewer emails than expected - Contact form
email: expected 2, got 1
fix: Check the settings of whatever sends them, or update expect in .echo/config.yaml if the change is intended.
An event counts only on its own channel: analytics: {purchase: 1} fails when the purchase event is missing, even if a page_view arrived (Sent fewer "purchase" analytics events than expected, analytics purchase: expected 1, got 0). With expect, an action is done as soon as everything it expects has arrived and nothing new comes for a moment, so it's faster than waiting for quiet. One that falls short waits up to timeout for the missing message. Its progress line shows ✗ and ends with fewer than expected. Errors (exit 2): line 4: expect: "mail" isn't a channel; use email, sms, whatsapp, chat, webhook, analytics, payment, push, http.
ignore: ["line_items[].meta_data", "x-request-id", "*@agency.example"]What compare leaves out for this action before it diffs: payload fields (order.total, line_items[].sku; a field takes its children with it), header names and recipients that match. Patterns are globs (*, ?, [ ]), case-insensitive, with [] meaning "every item of a list". Use it for a value that's different on purpose every run; to turn a whole check off, use checks: instead (Checks and rules). A pattern that doesn't parse is refused: line 4: action "A": ignore: "items[.sku" isn't a valid pattern (unclosed [ or a trailing \).
Values in a command and in an http action's URL, form, JSON, body and headers can use:
| Value | Becomes |
|---|---|
${fake:email} |
echo-test+3f2a01@example.com |
${fake:name} |
echo-test 3f2a01 |
${fake:phone} |
a +1555… number |
${fake:tag} |
3f2a01 |
${env:NAME} |
the environment variable NAME
|
The tag is random and the same for every action in one run. compare recognizes these values (and Soundcheck's soundcheck-test data) as test data, so a new address each run isn't a change.
Warning
.echo/config.yaml is code. A command action or a flush entry runs in your shell on every snapshot, compare and run. ${env:NAME} sends an environment variable to the URL an http action names. Read a config from a repository you didn't write before you run Echo in it, as you would a Makefile.
WooCommerce webhooks and many notifications are sent later, by WP-Cron, Action Scheduler or a queue worker. flush: lists commands Echo runs after each action, in the project folder and with the action's RHYTHM_ECHO_RUN, so that work is sent while the action is still open:
flush: ["wp cron event run --due-now", "wp action-scheduler run"] # WordPress
flush: ["php artisan queue:work --stop-when-empty"] # LaravelA flush that fails is noted (flush "…": exit status 1) and doesn't fail the action. In a parallel run, one action's flush runs at a time. rhythm echo doctor runs each flush command once to check it works. On WordPress, DISABLE_WP_CRON on staging makes runs repeatable: cron then only runs when Echo flushes it.
Mail that still arrives after its action ended, tagged with that action or dated before the next one began, is counted with the action that caused it. The run says so:
note: Checkout: email message e16e4deefac4 arrived 1.5 s after the action ended and is counted with it
note: Some mail arrived after its action ended; add a flush: command to .echo/config.yaml (like wp cron event run --due-now or php artisan queue:work --stop-when-empty) so queued senders send during their action
With flush: set, the second note reads …although the flush: commands ran; check they cover every queue the site uses, or raise quiet. Mail that arrives after the last action of a run isn't caught.
After triggering an action and flushing, Echo waits until nothing new has arrived for quiet (default 2s), up to at most timeout (default 30s), then records what came in. Raise quiet when messages trickle in slowly; lower it to speed up a long list of actions. An action's own quiet: replaces the global one for it. --quiet 500ms and --timeout 10s on snapshot, compare and run replace both for one run (-q alone still means "results only").
Echo's limits: quiet at most 30s and timeout at most 1m, with quiet no longer than the timeout. A value outside them is refused, in the config or on the command line:
rhythm echo: /tmp/t2/.echo/config.yaml: line 1: quiet: 45s is over the 30s Echo waits for quiet at most
rhythm echo: /tmp/t2/.echo/config.yaml: line 2: quiet: 20s is longer than the timeout of 10s (line 1), so no action could ever fall quiet
rhythm echo: --quiet 45s: over the 30s Echo waits for quiet at most
- Echo opens the action with a lease of its own: messages that arrive without a run ID are attributed to it.
- It triggers the action, tagged with the run ID.
- It runs each
flushcommand. - It waits, as above.
- It closes its lease and checks each message it caught (see Messages and checks).
A lease that isn't closed (a runner killed mid-action) runs out after 10 minutes; a long action renews its lease every 3 minutes. Two runs sharing one Echo, such as two CI jobs, never close each other's action; plain SMTP mail with no run ID that arrives while both are open is marked ambiguous and the run says so (1 message could not be tied to an action because another run was open).
Actions run in the order of the file. --action <name> (repeatable, ignoring case) runs only some of them; a name that isn't in the config is a usage error, no action named "x"; the actions are: Contact form, Checkout:
rhythm echo compare --action "Order shipped" --action "Weekly digest"note: 1 action from the snapshot wasn't run this time
--parallel <n> runs up to n actions at once. Echo widens only when it's safe: actions run one by one until a message from a connector shows up tagged with this run (WordPress, Laravel and Node tag every message), then up to n at once. If the first action sent nothing tagged, the rest run one by one with an exclusive lease, so untagged mail (plain SMTP, sendmail) can't be credited to the wrong action. An action with parallel: false waits for every other one to end and runs alone. Results stay in the file's order. Against an Echo without leases, actions run one by one, with a note: actions ran one by one: running them in parallel needs a newer Echo that keeps a lease per action.
production: lists your live sites, as hosts or URLs: ["shop.com"] covers https://shop.com, http://www.shop.com and any other subdomain, with any scheme on the default ports. An entry with a port or a path matches only that port or path. snapshot, compare and run refuse a site: under it before they contact anything (exit 3). Each http and replay action refuses a URL under it:
rhythm echo: refusing to run against http://staging.shop.test: it matches "shop.test" under production; actions only run against staging and local sites
doctor fails on the same site; Replay refuses it too.
Every action gets an ID of the form <run>/<action>, such as 2026-10-24T08-29-07-cd7ff3/action:contact-form: the run's start time with a random suffix, then action: and the action's name, lowercased, with runs of other characters turned into -. Renaming an action makes it a different action to compare.
The ID reaches the site in several ways. Connectors look in this order:
-
The
X-Echo-Runheader on the request that triggers the work. Echo'shttpactions and Soundcheck send it. -
The
X-Echo-Runcookie, the same value URL-encoded, for handlers that read cookies but not headers. -
Queued work carries it on: the WordPress connector stores it with each WP-Cron event and Action Scheduler action scheduled during the request, Laravel keeps it in the job's Context and the Node connector in
withEchoRun()scopes. -
RHYTHM_ECHO_RUNin the environment, for commands and workers.
When Echo knows a token, the header and cookie carry a signature (<run>/<action>.<sig>). A connector with the same token ignores a value without it, so a visitor can't add messages to a snapshot or take them out. See Run IDs and signatures.
A message with none of these still lands in the action that's open when it arrives, which covers plain SMTP mail and a cron job Echo flushed. Messages that arrive outside any action (rhythm echo watch while you click around) belong to no run.
A tool of your own can do the same: send X-Echo-Run: <run>/<action> on your requests, or open and close the action through the API (POST /v1/actions/begin, POST /v1/actions/end); see API.
snapshot runs every action and saves the run as .echo/snapshots/<name>/run.json, with a report.html. Without --name, the name is the date and time, such as 2026-10-24T08-19-22. Names use up to 64 letters, digits, dots, dashes and underscores, starting with a letter or digit. latest (in any case) is taken, as are Windows device names (CON, NUL, COM1…) and names ending in a dot: rhythm echo: --name "latest": use letters, digits, dots, dashes and underscores (LATEST is taken). Names that differ only in case are one snapshot. A snapshot with the same name is replaced once the new one is complete; the newest snapshot becomes the latest, the one compare uses.
rhythm echo snapshotsNAME ACTIONS TAKEN SENT
before-update 2 2m ago 3 emails, 1 SMS, 1 chat message, 2 webhooks, 2 analytics events (latest)
rhythm echo compare --snapshot <name> compares with another one. rhythm echo snapshots delete <name> (or rm) removes one. Deleting the latest snapshot makes the newest remaining one, by the time it was taken, the latest:
$ rhythm echo snapshots delete after-update
Deleted snapshot after-update
The latest snapshot is now before-update
A snapshot is written to .echo/snapshots/.incoming-<name> while it runs and moved into place when it's complete, so a snapshot that fails leaves nothing behind. When every action failed, nothing is saved and the old snapshot of that name stays the latest (exit 3):
[1/1] Down: nothing sent (failed: Get "http://127.0.0.1:9/contact": dial tcp 127.0.0.1:9: connect: connection refused)
rhythm echo: not saved: every action failed; fix the site or pass --force
--force saves it anyway (rhythm echo: every action failed; saved anyway because of --force, still exit 3).
A snapshot reports the problems the site has now (an unfilled placeholder already in a template), so you know the starting point; compare then reports only what changed.
A snapshot taken in one place can be compared in another, such as the base branch's CI job and the pull request's:
rhythm echo snapshot --export base.tgz # the latest snapshot, or --name oneExported snapshot before-update to base.tgz. Elsewhere: rhythm echo compare --against base.tgz
rhythm echo compare --against base.tgz # compare with it directly
rhythm echo snapshot --import base.tgz --name from-main # or add it as a snapshotImported snapshot from-main from base.tgz; compare uses it now.
--against takes a .tgz from --export, a run or snapshot folder or a run.json. --import makes the snapshot the latest; one of the same name is refused (rhythm echo: snapshot before-update already exists (--force replaces it, --name imports it under another name)). An archive is checked before anything is unpacked: only run.json and comparison.json are kept. Paths that point outside, links, more than one snapshot, more than 100 entries or 128 MB are refused with not a snapshot archive: <why>. --export - writes to stdout. Export and import run no actions, so they take only --name, --force, --config and --store.
compare runs the actions again from the current config, matches each action by its ID and diffs the messages each one sent. An action the snapshot doesn't have is run too. Everything it sends is listed as NEW, without checking those messages for problems (run shows them); actions missing this time are noted.
| Class | Means | Changes |
|---|---|---|
| BROKE | Worked in the snapshot, fails or is missing now | a message is no longer sent; a message now fails; the action itself fails; a webhook or payment payload lost fields; an attachment is gone or empty; a new error in a message (an unfilled placeholder, a PHP warning, a failed expect) |
| NEW | Wasn't there before | a message is now sent; a new attachment; a new warning or notice |
| CHANGED | The same message with differences: check it's intended | recipients' copies, subject, sender, Reply-To, text, event, payload fields or values, an attachment's type or size, fewer or more sends |
| FIXED | A problem from the snapshot is gone | an error, warning or notice that's no longer reported |
| Check | Class | Title | Before → after |
|---|---|---|---|
message/missing |
BROKE | Email to owner@shop.example.com is no longer sent |
sent → not sent
|
message/failing |
BROKE | Twilio SMS to the test number now fails |
faked → failed
|
run/action-failed |
BROKE | Action fails, Action still fails | the error |
message/new |
NEW | Email to office@shop.test is now sent | the detail is its subject or text |
message/fewer |
CHANGED | Email to the test submitter is sent fewer times |
2 times → 1 time
|
message/more |
CHANGED | Email to team@shop.test ("Weekly digest") is sent more times |
1 time → 2 times; fix: check for a hook or plugin that fires twice |
message/subject |
CHANGED | Email to the test submitter: subject changed | the subjects as sent |
message/from |
CHANGED | …: sender changed | the senders; fix: check it with rhythm echo check <domain>
|
message/copies |
CHANGED | …: copies (Cc/Bcc) changed | the copies |
message/reply-to |
CHANGED | …: lost its Reply-To | the old Reply-To |
message/text |
CHANGED | Twilio SMS to the test number: text changed | a line diff |
message/event |
CHANGED | …: event changed | the events |
message/fields-lost |
BROKE for webhooks and payments, CHANGED otherwise | WooCommerce webhook to gate ("order.created"): payload lost fields | the fields, e.g. billing.email
|
message/fields-added |
CHANGED | …: payload has new fields | the fields |
message/payload |
CHANGED | …: payload changed | a diff of the JSON, when the fields are the same and a value isn't |
message/attachment-lost, -changed, -new
|
see Attachments | Email to jamie@shop.test no longer attaches invoice-1042.pdf | the file → not attached
|
Issues found in messages (see Messages and checks) keep their own check names: a new content/placeholder error is BROKE, a new email/no-subject warning is NEW. When a message gets a new placeholder or PHP error, its text change isn't listed again.
A changed text is shown as a line diff of what a reader sees (for an HTML-only email, its visible text), with the first place that changed under the title and -v showing every place:
[CHANGED] Twilio SMS to the test number: text changed - Checkout
line 1 changed: Bean & Leaf: order #1002 is confirmed. Track it at https://shop.example.com/t/1… → 🎉 Hi Jamie! Thanks so much for your order #1004 with Bean & Leaf ☕️ We’re roast…
- Bean & Leaf: order #1002 is confirmed. Track it at https://shop.example.com/t/1002
+ 🎉 Hi Jamie! Thanks so much for your order #1004 with Bean & Leaf ☕️ We’re roasting your beans now and they’ll ship tomorrow. Track it any time at https://shop.example.com/t/1004 🚚
fix: Read the new text; if the change is intended, take a new snapshot.
An action that fails in this run is BROKE run/action-failed and what it would have sent isn't compared, so the verdict says the comparison is incomplete instead of claiming nothing changed:
[BROKE] Action fails - Order
"test ! -f /tmp/fail && printf \"To: ann@shop.test\\nSubject: Order received\\n\\nThanks.\\n\" | rhythm-echo sendmail -t -i\n": exit status 1
fix: The request that triggers the messages failed, so nothing it sends was compared: fix that first.
Could not compare: 1 of 2 actions failed in this run - 1 broke, 0 new, 0 changed, 0 fixed - 2 actions run, 1 email
It's Action still fails when it failed in the snapshot too; one that failed in the snapshot and works now is FIXED, with the note "<action>" failed in the snapshot, so what it sends now wasn't compared: take a new snapshot. When every action fails, compare exits 3: rhythm echo: every action failed, so nothing was checked; is the site up?.
A site whose connector can't reach Echo fails its sends while sealed, so the action fails and nothing arrives. Then each message the connector sent in the snapshot is listed as BROKE message/missing too, with a fix that says where to look: A connector that can't reach Echo blocks every send while sealed: check the site reaches Echo with rhythm echo doctor.
Two messages are the same message when they have the same identity:
- email, SMS and WhatsApp: the channel, the service and the recipients;
- everything else: the channel, the provider, the method, the host, the path with IDs replaced by
{id}(/v1/orders/123and/v1/orders/456match) and the event.
Several messages can share an identity, such as a welcome email and an account email to the same address. Within an identity, messages are paired by subject (and event) first, then whatever is left in order, so a changed subject reads as a change rather than one message lost and another new. Each pair is compared for content. A message left over in the snapshot is no longer sent (sent → not sent). One left over in this run is now sent, unless it's the same message as one already paired, which reads as sent fewer times or sent more times. When several messages share an identity, titles add the subject: Email to team@shop.test ("Weekly digest") is sent more times. A Zapier, Make or n8n hook is named by its masked URL, since each URL is its own Zap or scenario.
Before anything is compared, what varies on every run is normalized away:
| What | Becomes |
|---|---|
echo-test…@… and soundcheck-test…@… addresses |
"the test submitter" |
+1555 followed by seven digits and +15005550… numbers |
"the test number" |
| The subject | numbers replaced by # (#1042 becomes #) and test names by {name}, with case ignored, so Order #1041 and Order #1042 match |
| The text | dates and times in the usual forms, order, invoice, receipt, booking and ticket numbers after those words, coupon codes, UUIDs, long tokens, test data and whitespace; digits on their own don't count, so a moved price isn't a change either |
| A JSON payload | its field paths (order.total, line_items[].sku); a form body, its keys. Values are diffed as indented JSON for message/payload. |
So a new order number in the receipt, a fresh test address or a different timestamp isn't a change; a different admin address, a new subject line or a lost payload field is. A link to the staging host in a line that's otherwise the same is reported once, as the content/dev-link finding, not also as a text change. A payload cut to the size cap isn't compared field by field: <message>: payload too large to compare fields (cut at 256 KiB).
Each change has a key, eight hex characters that stay the same from run to run until the change itself differs. -v prints it under each change (key: 562b5cf7). --accept <key> (repeatable) takes a change as intended: it leaves the comparison and a note says so.
rhythm echo compare --plain --accept 562b5cf7 --accept df2d8e35 --accept 0000beefrhythm echo: --accept 0000beef matched no change
…
note: accepted as intended: SMS needs more than one part (df2d8e35)
note: accepted as intended: Twilio SMS to the test number: text changed (562b5cf7)
A key that matched nothing is reported on stderr (as above) and doesn't stop the run.
The same change in several actions is one line, listing every action it affects. Changes are sorted by class, then by how many actions they affect, then by title. At the end, the panel gives the verdict (Something broke, New messages or problems, Everything is still sent the same way, Could not compare) and the count per class.
Notes below the panel say what limits the result: the snapshot ran in seal mode and this run in observe mode, 1 action from the snapshot wasn't run this time (2 actions from the snapshot weren't run this time), 1 action isn't in the snapshot; what it sent is listed as new (2 actions aren't in the snapshot; what they sent is listed as new). A run during which Echo restarted says so in a warning that starts Echo restarted during the run; and ends run again with Echo up throughout, since messages it held in memory may be gone.
Without --fail-on, compare exits 1 on a BROKE change and run and snapshot on an error. --fail-on broke|new|changed sets the class for a comparison (new fails on BROKE or NEW, changed on anything but FIXED); --fail-on error|warning|notice sets the level for a run or snapshot. Any case works. An action that fails counts at every level. --fail-on none never fails on findings or failed actions; an action that can't run at all still exits 3. A value of the wrong kind is a usage error:
rhythm echo: --fail-on "error": want none, broke, new or changed
rhythm echo: --fail-on "broke": want none, error, warning or notice (or broke, new, changed when comparing)
See CI.
Every snapshot, run and comparison is stored in .echo/ with a standalone report.html. report renders a stored result again without running anything:
rhythm echo report # the newest run, comparison or snapshot
rhythm echo report before-update # a snapshot
rhythm echo report .echo/runs/2026-10-25T14-23-14-f6f2df
rhythm echo report --md -o echo.md
rhythm echo report --junit -o echo-junit.xml --fail-on new
rhythm echo report --open # open the HTML report| Format | Flag | |
|---|---|---|
| Terminal | (default) | The live view, styled; --plain (or any output that isn't a terminal) gives plain text with every detail. |
| HTML |
--html, -o report.html
|
One file: actions with their messages (emails in a sandboxed frame with remote images blocked until you click Load remote images, SMS and chat as bubbles, payloads as sent and each answer), changes on top with their diffs. |
| Markdown | --md |
The verdict, a count table, then a table per class with the change, actions, before → after and fix, plus a collapsed diff per text or payload change and a table of attachments. Fits a pull request comment or a job summary. |
| JSON | --json |
The JSON envelope with the comparison or the run in data; see CLI reference. |
| JUnit | --junit |
A test suite per class (echo.broke, echo.new…) and a test case per change; cases at or above --fail-on (BROKE only without it) are failures. |
-o <file> picks the format from the extension (.html, .md, .json, .xml) and still prints the terminal view; a format flag without -o prints only that format on stdout. Reports and the files under .echo/runs/ and .echo/snapshots/ are written readable by you only (0600, folders 0700). report of a comparison exits 1 when something broke, as compare does. Runs and comparisons go in .echo/runs/<date-time>-<id>/; the 20 newest are kept. Snapshots are never pruned.
← Attachments · Home · With Soundcheck →
rhythm · Useful ideas. Working systems. · Check only sites you own or have permission to test. Keep staging sealed (responsible use) · This wiki is published from wiki/ in the rhythm repository; suggest changes there with a pull request.
Get started
rhythm in depth
Tuner
- Overview
- Your first scan
- Reading results
- Choosing checks
- Requests and politeness
- Output and reports
- Batch scans
- Local scans
- Integrity and malware
- Changes and notifications
Tuner in depth
- Known vulnerabilities
- PHP advisories
- CI and code scanning
- GitHub Action
- Config file
- Watch and serve
- Writing checks
- Check packs
- Finding reference
- CLI reference
- Coming from Wordfence CLI or WPScan
- FAQ and troubleshooting
Soundcheck
- Overview
- Your first check
- Reading results
- Review and accept
- Discovery
- Page health
- Visual changes
- Interactions
- Forms and flows
- Cache
- Speed
- Accessibility and SEO
Soundcheck in depth
- CI
- CI baselines and PR comments
- Configuration
- Error rules and exec
- WordPress updates
- Fleet and client reports
- History
- Import from BackstopJS and Lost Pixel
- Firefox and WebKit
- CLI reference
- FAQ and troubleshooting
- Benchmark
Echo
- Overview
- Your first run
- Connectors
- Sealing staging
- Echo Inbox
- Messages and checks
- Checks and rules
- Attachments
- Actions and compare
- With Soundcheck
- Replay
- Deliverability
Echo in depth
- Docker and DDEV
- Agency hub and reports
- Schedule
- MCP server
- Conformance kit
- CI
- Configuration
- API
- CLI reference
- FAQ and troubleshooting
- Benchmark
Project