Credited skills
Skill 39 of 41
Tests an n8n Assistant (Instance AI) UI feature end to end.
13 minutes · 2,866 words · 14 sections
Use this skill to test a UI feature of the n8n Assistant end to end. In code, the Assistant is “Instance AI”. You draft the test paths, the user approves them, and the skill runs them all at the same time. A local page shows each run live. At the end, you judge each run and build one report.
The skill uses the scripts of testmax-n8n-ai for the n8n API, the
LangSmith usage, the live page server, and the report template. Do not copy
those scripts.
flowchart LR
A[Feature + diff] --> B[Draft scenarios.json<br/>and scripts]
B --> T[--try and --estimate]
T --> C{User approves<br/>plan + cost}
C --> D[run.py]
D --> E[One fresh branch instance<br/>for each feature + regression]
D --> F[Fresh branch instance<br/>visual]
D --> G[Fresh master instance<br/>visual, or a kept run]
E & F & G --> H[Live page]
F & G --> I[Pixel + accessibility tree diff]
E & F & G --> J[summary.txt with triage]
J --> K[You judge: verdicts.json]
K --> L[Report rebuilds itself]feature: tests a path of the new feature.regression: tests an existing flow that the feature touches.visual: captures screens. The skill runs it on the branch and on master,
then compares the captures.featureFlags.run.py builds this checkout first (with turbo, so a build that is up to
date takes about 1 second). The master instance starts from a git worktree
at ~/.cache/n8n-feature-test/master, at the merge base of HEAD and
origin/master. Thus the visual check shows only the changes of this
branch, not the changes that master got after the branch started.Run python3 .agents/skills/testmax-assistant-ui/scripts/run.py --check
before the first run. The commands below use $S for this scripts folder.
--check checks the tools,
Chrome, the settings, the disk space and the master build, and prints the
plan folder for this branch.
.env.local or .env.ai-surfaces, as for
your own instance. Fresh instances get the same settings, without the
license, the database, the port and the encryption key.LANGSMITH_API_KEY and LangSmith tracing, for the tokens and cost. Without
them, the report shows no tokens or cost. Do not set
N8N_DIAGNOSTICS_ENABLED=false: then Instance AI sends no traces.Never put a key on the command line. Read settings from the env files and the environment only.
Find what changed and what the user wants to test:
git diff origin/master...HEAD --stat -- packages/frontend packages/@n8n/instance-ai packages/cli/src/modules/instance-ai
git diff origin/master...HEAD -- <the files of the feature>Read the changed Vue components, the stores, and the backend events that they
use. Find the data-test-id values of the new elements. If the user did not
describe the feature, ask for one or two sentences about it.
If the plan has visual scenarios, start the master baseline now, in the background. The first build takes 15 to 20 minutes. Later builds take less than a minute when the turbo cache has the files. You can draft the plan while it builds:
S=.agents/skills/testmax-assistant-ui/scripts
python3 $S/run.py --check # the setup, and the plan folder
python3 $S/baseline.py prepare # in the background
python3 $S/baseline.py status # later: is it ready?Write the plan folder that --check prints:
~/.cache/n8n-feature-test/plans/<branch>/<feature>/. The folder stays after
a reboot. It holds scenarios.json and one script for each scenario. The
folder name is the name of the run folders.
Draft these paths:
smoke-chat for the plain chat reply.visual-assistant-home and visual-thread-reply.Keep each scenario short and focused. A scenario that fails must show where it failed. Use more scenarios, not longer ones. A visual scenario must not need the new feature, because it also runs on master.
Some UI states show only for a moment, for example a status line that folds away when the run ends. Check them while the run is active, not after it.
If the feature is behind a feature flag or an experiment, set the flag in
featureFlags. Without it, the instances use the default value of each flag.
Before you show the plan to the user, check the new scripts and get the estimate (see Check the scripts). Then show the user the list of paths: the id, the kind, the title and the expected result of each. Also show the estimate: the number of LLM runs, the cost and the time. Change the plan until the user approves it.
{
"title": "Stop button in the Assistant",
"feature": "The user can stop a run while the Assistant works.",
"scenarios": [
{
"id": "stop-mid-run",
"title": "Stop a run in the middle",
"kind": "feature",
"script": "stop-mid-run.mjs",
"expected": "The stop button shows during the run. A click ends the run without an error message.",
"timeoutMin": 6
},
{
"id": "smoke-chat",
"title": "A prompt gets a reply",
"kind": "regression",
"script": "lib:smoke-chat.mjs",
"expected": "A short prompt gets the reply OK."
}
]
}The plan has these fields: title (required), feature (the feature, for the
judge), and featureFlags (optional). featureFlags maps a flag name to
true, false or a variant name, for example
{ "<flag name>": "variant" }. Each instance gets these flags, on the
branch and on master. They go into N8N_FEATURE_FLAG_OVERRIDES and replace
the values in your env files.
Each scenario has these fields:
| Field | Required | Description |
|---|---|---|
id | yes | Lowercase letters, digits and -. No --. Unique. |
title | yes | A short name for the report. |
kind | yes | feature, regression or visual. |
script | yes | A path relative to scenarios.json, or lib:<file> for a library script. |
expected | yes | The expected result, for the judge. |
autoConfirm | no | Answer the Assistant’s confirmations automatically. Default true. Credential and setup cards get “Later”. Set false when the script answers a confirmation or a card. |
isolated | no | Run the scenario on its own fresh instance. Default true. Instances start ahead of their scenario, so this costs almost no time. Set false to run the scenario on one branch instance that it shares with the other false scenarios. Visual scenarios ignore this field. |
timeoutMin | no | The time limit of the scenario. Default 10. |
viewport | no | The window size, for example 1600x1000 (the default). |
examples/stop-button/ is a full example plan.
A script exports one async function. It gets the kit t:
export default async function (t) {
await t.step('Open the Assistant', () => t.goto());
await t.step('Send a prompt', () => t.send('Reply with exactly the word OK and nothing else.'));
await t.step('Wait until the run ends', () => t.waitForIdle({ timeout: 180_000 }));
await t.check('The reply says OK', () => t.expect(t.ai.assistantMessages().last()).toContainText('OK'));
await t.capture('thread', { mask: [t.ai.assistantMessages()] });
}| Kit member | What it does |
|---|---|
t.step(title, fn) | Runs an action. Records a screenshot and the time. A failure stops the scenario. |
t.check(title, fn) | Runs an assertion, with a screenshot. A failure is recorded, and the scenario continues. |
t.note(title) | Adds a step with a screenshot and no action. |
t.goto(path = '/assistant') | Opens a page and waits for the chat input. Closes the “Try new n8n Assistant” dialog. |
t.send(text) | Types into the chat input, sends, and waits for the thread URL. Records the thread for the trace. |
t.waitForIdle({timeout, settle}) | Waits until the thread has no active run. Returns 'idle', or 'suspended' when the run waits for the user. |
t.capture(name, {mask, maskText, root, fullPage}) | Saves a screen and its accessibility tree for the visual check. mask takes locators, and maskText takes texts: all elements with that text. Masked areas show as one color. In the tree, the subtree of a mask element and each masked text show as [masked]. |
t.api(method, path, body) | Calls the n8n REST API as the signed-in user. Returns data. |
t.threadStatus(threadId) | Returns {active, suspended, background}: the thread has a run, the run waits for the user, and the number of running background tasks. |
t.ai.* | Locators for the Assistant: chatInput, sendButton, stopButton, userMessages, assistantMessages, assistantText(text), panelText(text), sidebarToggle, threadList, newThreadButton, approveButton, denyButton, planApproveButton, previewPanel, previewCanvasNodes, and more in scripts/kit.mjs. |
t.expect, t.page, t.baseUrl, t.sleep(ms) | Playwright expect, the page, the instance URL, and a pause. |
Rules for scripts:
data-test-id locators (page.getByTestId). Use text only for text that
the test controls.t.expect(...).toBeVisible() and other assertions that wait. Do not use
t.sleep to wait for the UI."autoConfirm": false, wait with
t.waitForIdle() until it returns 'suspended', then click the button.Check the new scripts on fresh instances before you show the plan:
python3 $S/run.py --plan <plan>/scenarios.json --try --only id1,id2--try does not run master, does not start the live page, and builds no
report. It prints a table with the status, the steps and the first error of
each run. The runs are in /tmp/feature-test/try/. Fix the scripts that fail
because of the script, then try them again. Each LLM scenario in a try costs
money too, so try only the new scripts.
Then get the estimate for the full run:
python3 $S/run.py --plan <plan>/scenarios.json --estimateThe estimate uses the cost and time of earlier runs of the same scenario ids.
A scenario without an earlier run shows * and the median of all earlier
runs. A master run that is kept shows (kept) and costs nothing.
Open the live page for the user on each run. Do not skip this step. Do this:
Find out if your harness has a browser tool that opens a tab in the
editor. In Cursor, this is browser_navigate of the cursor-ide-browser
MCP server.
If it has one, start the run in the background with --no-open:
python3 $S/run.py --plan <plan>/scenarios.json --no-openWait for the live page: <url> line in the output, which comes in a few
seconds. Then open that URL in a new editor tab that the user can see (in
Cursor: newTab: true and position: "active"). If the tab does not open,
run open <url> (macOS) or xdg-open <url> (Linux).
If it has none, start the run in the background without --no-open.
run.py opens the live page in the default browser:
python3 $S/run.py --plan <plan>/scenarios.jsonGive the user the URL in both cases.
run.py does not read $BROWSER, so do not set it.
run.py does these steps:
/tmp/feature-test/<time>-<plan folder name>/.--no-open, it opens the
page in the default browser.build.log.summary.txt,
and builds the report without verdicts.runDir, report, live, summary, results
(the status of each run), visual (the number of captures, the changed
ones, and the error when the visual comparison failed), rerun and
next.| Option | Use |
|---|---|
--check | Check the setup and print the plan folder, then stop. |
--estimate | Print the expected cost and time, then stop. |
--try | Check new scripts: no master, no live page, no report. See Check the scripts. |
--only id1,id2 | Run only these scenarios. |
--rerun last | Run the plan of the last run again, in a new folder. |
--rerun <run-dir> | Run the plan of this earlier run again, in a new folder. If the plan folder of that run still exists, it uses the current plan and scripts from there. Thus a fixed script runs in its fixed version. |
--concurrency N | Runs at the same time. Default 4, or FEATURE_TEST_CONCURRENCY. |
--branch-url URL | Use a running instance for all branch runs, not fresh ones. The threads stay on that instance. The scenarios share its data, and the visual check is not reliable. |
--no-build | Do not build this checkout. The output warns when source files are newer than their build. |
--no-baseline | Do not run master. There is no visual check. |
--no-cache | Run master again, also when a kept master run exists. |
--no-fetch | Do not fetch origin/master. Use the merge base with the origin/master that git has. |
--no-open | Do not open the live page in the default browser. Use this when you open it in the editor tab. |
--no-live | Do not start the live page. Use this only when the user asks for it. |
--keep-instances | Keep the fresh instances running after the run, for a manual check. The output gives instancePorts. Each instance is at http://localhost:<port>. Stop them yourself. |
--run-dir DIR | Use this run folder. |
| Setting | Default |
|---|---|
FEATURE_TEST_RUNS_DIR | /tmp/feature-test |
FEATURE_TEST_CACHE | ~/.cache/n8n-feature-test: the plans, the master worktree, the kept master runs, and the instance data templates |
FEATURE_TEST_CONCURRENCY | 4 |
The live page shows each run as a row, with its status and its last screenshot. The screenshot changes every few seconds. When the run is done, the same address shows the report. The page stops 30 minutes after its last request.
Read <runDir>/summary.txt. It starts with a triage: each run is clean, or
look closer with the reasons (a failed step or check, a browser error, a
failed tool call, or a visual change). Then it has, for each run: the expected
result, the steps and their status with the path of each screenshot, the
prompts, the last reply, the browser errors, and the visual results.
For a clean run, look at the last screenshot and check it against the expected
result. For a run to look at closer, look at the screenshots of the steps in
the reasons, and the diff images in visual/<id>/.
A script that passed is not always a pass. Check that the run shows the expected result, not only that the script ended. Check for these problems:
Write <runDir>/verdicts.json. Use the run names as keys. A master run has
the name <id>--master, and does not need a verdict:
{
"stop-mid-run": {
"verdict": "pass",
"summary": "One sentence about the result.",
"checks": [
{ "ok": true, "text": "The stop button ends the run" },
{ "ok": false, "text": "The send button comes back after 9 seconds" },
{ "ok": null, "text": "Could not confirm the stopped label" }
]
}
}Use pass when all checks pass, fail when the main goal is not met, and
partial for all other results. For a visual scenario, judge each change:
intended by the feature, a regression, or noise (for example, a timestamp
that is not masked). Be strict and factual.
Write each check as one short sentence with one fact. Put selectors, test IDs and UI text in backticks. The page shows them as code. The page shows the failed checks first.
While the live page runs, it builds the report again each time
verdicts.json changes. The open tab loads the new report by itself, and
keeps the selected run and the scroll position. Thus you do not need to load
the page again. When the live page does not run, build the report yourself:
python3 $S/report.py <runDir>The report is <runDir>/report/index.html. It is one folder with all its
files, so you can copy or share it. It has, from top to bottom:
Baseline, and a run without a verdict shows Not judged, or
Failed when its script failedEach box shows closed. It opens and closes when you click its title. On the live page, a box that you open or close stays that way when the page updates.
A master run that is kept from an earlier run shows “Reused” in its run box.
Give the user the path and the live URL. Tell the user the result of each
scenario, the visual changes, and the problems that you found.
| Step | Command |
|---|---|
| Prepare the master baseline | python3 $S/baseline.py prepare |
| Show the baseline state | python3 $S/baseline.py status |
| Run one script on any instance | N8N_BASE_URL=<url> node $S/run_scenario.mjs --out <dir> --script <file> |
| Compare the visual captures again | node $S/visual_diff.mjs <runDir> |
| Print the judge summary | python3 $S/report.py <runDir> --summary |
| Serve the live page of a run | python3 $S/live_page.py --run-dir <runDir> |
--rerun last --only <id> before you call
it a bug.git worktree list. To remove it, run
git worktree remove ~/.cache/n8n-feature-test/master.~/.cache/n8n-feature-test/templates change when the
migrations change. You can delete old templates at any time./tmp, so the system
deletes them on reboot.Credited
This skill is installed in n8n-io/n8n — in use here rather than published from here — so there is no install command for it on this page.
Tests an n8n Assistant (Instance AI) UI feature end to end. Drafts the test paths from the feature and the diff, writes one Playwright script for each path, runs the paths in parallel on fresh n8n instances with a real LLM, and compares the main Assistant screens with master to find visual regressions. Shows live screenshots during the run, and builds one HTML report with video replays, step screenshots, LLM steps, tokens, cost and visual diffs. Use when the user wants to test an Instance AI or Assistant UI feature end to end, check that a UI change does not break existing screens, or says /testmax-assistant-ui.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
master, last pushed 8 November 2026.SKILL.md, not by matching a directory convention. 6 distinct layouts observed: .agents/skills/*/SKILL.md, .claude/plugins/n8n/skills/*/SKILL.md, .opencode/skills/*/SKILL.md, docs/generated/*/skill.md, packages/@n8n/cli/skills/*/SKILL.md, packages/@n8n/instance-ai/skills/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by n8n, declaring 1 plugin. It is read for editorial metadata only — never as the skill index, which is always the repository tree./n8n-io/n8n.md, and each skill at its own .md URL.13 files · 113 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 39.
Executable code the skill can run.
Everything else published alongside the skill.