Six defects in a verified n8n node for IBM Quantum
It passed n8n verification with green tests and 98 percent coverage. Then 121 seconds of real QPU time found what none of that could.
The error told me my circuit contained a gate my circuit did not contain.
Failed to execute program: 'Circuit 0: The instruction u on qubits (1,) is not
supported by the target system. -- Transpile your circuits for the target before
submitting a primitive query. reason_code: 1517
I read it three times. There was no u anywhere in that circuit. I had built it deliberately out of the gates ibm_kingston reports as its own native set, which is the whole point of an ISA circuit, and the machine was refusing an instruction I had never written.
That was the start of eight hours that turned up six defects in my IBM Quantum node for n8n, three of them reachable only by putting circuits on real hardware. The package they were sitting in was already verified by n8n. Every unit test passed. Statement coverage was 98.55 percent. npm audit was clean. None of that had anything useful to say about any of the six.
What did have something to say was 121 seconds of QPU time across two IBM machines. This is what it found.
The gate that lies about itself
The circuit that produced that error contained an Identity gate. So I checked whether id was native on that backend.
basis_gates ibm_kingston: ["cz","id","rx","rz","rzz","sx","x"]
It is right there. IBM's own backend, reporting id among its native gates, on the same device that had just refused the job. That made it worse, not better. A user building an ISA circuit sees that list, includes id because the vendor says the chip runs it, and the job dies naming an instruction that appears nowhere in their program.
The cause is in the language, not the hardware. stdgates.inc, the OpenQASM 3 standard library, defines identity as gate id a { U(0, 0, 0) a; }. IBM's parser expands it into the builtin U. The target refuses U. The name matches a native instruction, and the path never reaches it.
A theory that sounds right is the most expensive kind of wrong, so I put it on hardware. Three control jobs, one variable at a time:
| circuit | result |
|---|---|
reset + x + rz + cz + barrier + measure | completed, {"01":120,"00":8} |
rx(pi) alone | completed, {"1":123,"0":5} |
id + x + measure | failed, instruction u |
Six seconds of quota to turn a theory into a fact.
The fix keeps Identity in the palette and still validates its operands, so a wrong qubit index is still an error rather than a silent drop, but it emits nothing at all. Identity is the no op, so what comes out is mathematically identical and it runs. Same circuit that had failed:
QASM emitted: qubit[2] q; bit[2] c; x q[0];
c[0] = measure q[0]; c[1] = measure q[1];
status: completed
counts: {"01": 249, "00": 7}
Reproduced on ibm_kingston and on ibm_marrakesh.
If you take one thing from this post, take this. basis_gates tells you what the chip can execute. It does not tell you what the parser will hand the chip. A gate the vendor lists as native can still be unreachable, because of how the standard library of the language happens to define it, and nothing in your local test suite will ever tell you.
A gate I built, tested and then deleted
Look at that basis gate list again. rzz is in it too.
So I implemented it properly: arity, rendering, palette entry, unit tests, build, redeploy into n8n. Then onto real hardware, at two different angles so a bad parameter could not be the explanation.
RZZ(pi): failed "Error parsing OpenQASM program"
RZZ(pi/2): failed "Error parsing OpenQASM program"
Parse error, not target error. IBM's OpenQASM parser does not recognise the symbol at all. It never gets far enough to ask the hardware.
At that point I had a suspicion and no proof, which is the same position I had been in twenty minutes earlier with id, so I did the same thing. Same circuit, rzz removed, nothing else touched.

d9nl060qs0bc73e3ifvg, the same ID tagged qa-rzz-control in the console at the top of this post.status: completed
counts: {"00":480,"01":32} -> 93.8% "00"
Which is exactly what the circuit predicts, since two Hadamards cancel. The program was fine. Only rzz broke the parser.
The console screenshot at the top of this post holds all three of those jobs in one frame, consecutive rows on ibm_kingston: qa-rzz-pi failed, qa-rzz-half failed, and directly beneath them qa-rzz-control completed.
So I deleted the implementation. All of it, after building it. Deleting working code you have just finished feels like waste.
It was not waste. Shipping it would have recreated the precise trap the id fix had just closed: a gate sitting in the palette, listed as native by the backend, failing every job it ever touched. The only difference is that this time I would have put it there myself. The knowledge stayed, written into the README and the CHANGELOG. The code went.
The third thing only hardware could tell me
The first GHZ-40 run failed on a restriction that is easy to trip and easy to miss:
Gate twirling does not support fractional gates. -- If the PEC, PEA, or gate
twirling option is selected, then make sure to remove all fractional gates
from the circuits.
Every circuit Qiskit transpiles for a Heron processor uses parametrised rx, which IBM classifies as fractional. Which means anybody following the standard workflow, transpiling with Qiskit and ticking Gate Twirling because more error suppression sounds better, loses the job. Dynamical Decoupling and Measurement Twirling carry no such restriction.
That warning now lives in the parameter description itself, where a user reads it inside n8n at the moment it matters, rather than in a document nobody opens.
Not one of those three things was reachable from my laptop: a defect in my own code, a gate I deleted after building it, and a restriction inside IBM's stack. That is the honest case for spending real quota on your own tooling.
The next one is the reverse. Hardware confirmed it. Hardware could never have caused it.
A measure that wrote outside its own register
This defect exists only because the node can be called by an AI Agent.
A measure gate with no value in the Classical Bit field passed validation and produced a program IBM rejects. Two code paths used different fallbacks for the same missing value. Validation checked clbit ?? 0. Rendering used clbit ?? targets[0]. Reproduced against the compiled dist before the fix:
validateGateInput -> VALID (no error)
QASM emitted:
qubit[3] q;
bit[1] c;
c[2] = measure q[2]; <- writes to bit 2 of a one bit register
That field can only go missing when the gate object is built outside the n8n editor. By a model calling the node as a tool, by an imported workflow, or through the public n8n API. Inside the editor it always carries a value, so no amount of clicking would ever have surfaced it.
The fix normalises to 0 in mapGate and uses the same fallback in the renderer, so the two cannot drift apart again. Confirmed on hardware: the test emitted c[0] = measure q[2]; and the job completed.
The general form is worth stating plainly. If your node sets usableAsTool, every optional field becomes a field a model can omit. Defaults that live in the interface are not defaults.
Physics does not care what you believe
Every job ran through a local n8n 2.32.7 instance with the node installed exactly the way n8n installs a community package: npm pack, extract the real tarball into .n8n/nodes/node_modules, npm install --legacy-peer-deps. Not npm link. If the test does not use the install path real users get, it is not a test of the thing they receive.
The reason quantum circuits make an unusually good test suite is that the answer is known in advance and the machine has no interest in agreeing with you. If any link in the chain is wrong, from building the PUB to decoding hex samples back into counts, the numbers simply do not arrive.
| test | predicted | measured |
|---|---|---|
| Estimator ⟨Z⟩ on |1⟩ | -1 | -1.0024 |
Estimator, Pauli array ["ZI","IZ","ZZ"] | +1, -1, -1 | +1.003, -1.014, -1.025 |
Estimator, coefficient map {"ZI":1.0,"IZ":0.5} | +0.5 | +0.472 |
Parametrised rx(theta) at θ = π | |1⟩ | {"1":125,"0":3} |
The parameter sweep is the strongest single piece of evidence in the whole campaign. One submission, five bindings, and a cosine came back out of a superconducting chip:
theta predicted cos(t) measured <Z> delta
0.0000 1.0000 1.0013 0.0013
0.7854 0.7071 0.7262 0.0190
1.5708 0.0000 0.0135 0.0135
2.3562 -0.7071 -0.6946 0.0125
3.1416 -1.0000 -1.0058 0.0058
Maximum deviation 0.019. You cannot fake that curve with a broken parser.
Two parsing details came out of the same campaign, and both were silent failures waiting to happen.
num_bits was the last open question, and it got answered on real data. The samples arrive as 0x1 and 0x0. If the API had not sent num_bits, my width inference would have returned 1, and the results would have read {"1":250,"0":6} instead of {"01":250,"00":6}. The second bit would have disappeared without a trace and every count would still have summed correctly. It is present, at 2 and at 64.
BigInt earns its place at 64 bits. The wide register test used 2⁶³ + 1. As an IEEE double that rounds to 2⁶³ and the low bit vanishes, so parseInt would have produced 1000...0000. The parser produced 1000...0001.
I also built a Bell state out of native gates with no transpiler anywhere, using the two identities that do most of the work: H equals rz(π/2) then rx(π/2) then rz(π/2), and CNOT from control to target equals H on the target, cz, H on the target again. Thirteen gate entries, on ibm_marrakesh, 2048 shots:
| outcome | count | share |
|---|---|---|
00 | 1046 | 51.1% |
11 | 946 | 46.2% |
01 | 25 | 1.2% |
10 | 31 | 1.5% |
97.3 percent correlated, 2.7 percent readout noise, 3 seconds of quota. If you want the matrices behind those two identities, I worked them by hand in an earlier post.
Eighty qubits, depth 241, and samples too wide for a double
My README had been recommending Qiskit for transpilation, and that recipe had never been verified end to end. So I ran it. Qiskit 2.2.3 in an isolated environment, and the real topology of ibm_kingston read through the node itself with Backend then Get Configuration, which returned 352 coupled pairs and the native gate list. Nothing assumed.
| circuit | logical depth | transpiled depth | CZ gates | shots | result | cost |
|---|---|---|---|---|---|---|
| GHZ-12 | 13 | 37 | 11 | 8,192 | 37.7% |0¹²⟩, 34.3% |1¹²⟩ | 4 s |
| GHZ-40 | 41 | 121 | 39 | 15,000 | ranks 1 and 2 of 4,853 outcomes | 6 s |
| GHZ-80 | 81 | 241 | 79 | 25,000 | ranks 1 and 2 of 23,294 | 9 s |
| GHZ-80 | 81 | 241 | 79 | 45,000 | ranks 1 and 2 of 41,172 | 14 s |
The heaviest run, in full:
80 physical qubits, depth 241, 79 CZ gates, 45,000 shots
|0^80> : 0.48% rank #1
|1^80> : 0.40% rank #2
outcome space: 2^80 = 1.2 x 10^24 possible strings
every string exactly 80 bits : PASS
sum of counts = shots (45000) : PASS
payload: 1041 KB downloaded and parsed inside the 30 s timeout
Out of 1.2 quadrillion possible bitstrings, the two GHZ extremes come first and second. On a noisy processor, at depth 241, through 79 entangling gates. Every sample is wider than a double can hold.

All 24 operations went against live infrastructure, not only the interesting ones. Both polling triggers fired with the failure reason attached. Seven error paths. Sessions created, queried, closed, including one batch that genuinely carried two jobs rather than being opened and closed empty. Private jobs, where IBM redacts the submitted parameters and leaves the tags visible, which confirms the redaction is selective exactly as documented.
And one test I did not plan. Partway through, ibm_marrakesh went into maintenance, so I checked on the spot whether Get Least Busy would exclude it:
marrakesh state: message "maintenance"
Get Least Busy candidates: kingston (queue 2), fez (queue 5)
marrakesh excluded: PASS
ibm_fez is the only backend I touched without running anything on it. It was queried through the API and never received a job.
The filter that returned everything and never said so
The remaining defects are a different species. They were not failures at all. They were wrong answers, delivered confidently, with a 200 on them.
Filtering jobs by tag did not filter. It did not error. It returned everything.
n8n serialises an array of query parameters as tags[]=a&tags[]=b. IBM's API recognises repeated keys, tags=a&tags=b, and silently ignores what it does not recognise. No warning. Status 200, full list, every time.
BEFORE arrayFormat:
filter 'qa-audit' -> 20 jobs, first ones tagged "qa-ghz80-max"
filter with a nonexistent tag -> 20 jobs
AFTER arrayFormat: 'repeat':
filter 'qa-audit' -> 1 job, tags ["qa-audit","retagged"]
filter 'qa-audit, retagged' -> 1 job (real AND)
filter with a nonexistent tag -> 0 jobs
The unit tests passed the whole time, because they asserted the shape of the request my code built. Nobody had ever asked the server what it did with it. This is the cleanest example I have of the distance between "the tests pass" and "it works", and it only closed because I checked a filter against the live API instead of against my own expectations.
Three smaller ones came from the same family of misplaced trust. parseResults guarded with ?? [], which catches only null and undefined, so a results field that was not an array reached .map and threw a bare TypeError instead of a readable n8n error. It was found by writing a test that failed, not by reading the code. The numeric fields trusted the interface, because minValue in n8n is a visual hint and an expression can deliver a string, a float or a negative straight into the request, so shots went into the PUB unclamped and wasted a submission. And Submit checked less than Import did: the literal string this is not qasm was accepted, received a job ID, queued, and failed on IBM's servers. That cost 2 seconds of quota to learn.
Coverage measures what ran, not what was right
The suite went from 140 tests to 185. Statements from 98.55 to 99.8 percent, branches from 89.81 to 97.83, lines to 100. Thresholds were raised from 85/85/80 to a set that sits directly under the measured figures, so a regression now trips CI instead of passing quietly. Ten branches remain uncovered and each is commented where it lives, because each is unreachable by construction.
Here is the part that changed how I read a coverage report. The measure bug was on a line reported as 100 percent covered. The existing test passed clbit explicitly, so the line executed every run and the fallback branch never did. Same story with parseResults.
Coverage tells you which lines ran. It has no opinion whatsoever about whether they were correct.
A failed circuit is not free
Worth knowing if you are on the Open plan and its 600 seconds a month.
IBM defines usage as the time a QPU is locked for your workload, and its documentation is explicit about failures: a job that fails from a system error reports zero usage, but a job that fails through user error, or is cancelled by the user, is charged for whatever was consumed up to that point, including the overhead of preparing the QPU to run it. The same page puts that per sub-job overhead at approximately 2 seconds.
That is exactly what I measured. Every one of my five deliberate negative tests, the invalid QASM, the id gate, both rzz attempts and GHZ-40 with Gate Twirling, cost precisely 2 seconds. Ten seconds of a monthly 600 spent on circuits designed to fail. That is a price worth knowing before you plan a campaign.
The same page defines batch usage as the cumulative QPU lock time of all jobs in the batch, which means the Batch rows in the console are aggregates of jobs already listed individually. Adding them to a per job total double counts.
What 0.3.3 ships, and what it took to publish it

n8n-nodes-ibm-quantum is three nodes and one credential, MIT, zero runtime dependencies. The source is on GitHub. Circuits travel as OpenQASM 3 strings and the IBM Cloud API key is exchanged for a short lived IAM bearer token at request time. Twenty four operations across Backend, Circuit, Job, Session and Account.
Four things are new, each verified against the live API rather than a mock:
- A Program filter on Job List. Sampler or Estimator. Checked live: 20 sampler, 12 estimator, empty returns both.
- Multiple Tags filters on Job List and both triggers, up to the eight the API accepts, with AND semantics. A tag that does not exist now returns zero jobs, which is what the old code could not do.
- An
unparsedSamplesfield that appears only when the parser cannot read part of a register, so a gap betweencountsandshotsis visible instead of silent. Absent on healthy data, confirmed present on the 80 qubit payload. - A documented recipe for ISA circuits without a transpiler, the one that produced the Bell state above.
Then there is the part that has nothing to do with quantum computing. I ran the official @n8n/scan-community-package against my own published package, the one already carrying the verified badge.
BEFORE (git HEAD = published 0.2.3): passed=false errors=10
AFTER (repaired working tree): passed=true errors=0
Ten errors, on a package that was verified and installed by other people. The scanner does not lint your working tree. It checks the provenance attestation, downloads the source from the commit that attestation points to, and lints it with eslint-plugin-n8n-nodes-base in addition to the community nodes plugin. My local config loaded only the second, and did not lint package.json at all.
One of the ten is worth showing in full, because it is the kind of rule you cannot guess at. Both trigger nodes failed the display name check, which accepts exactly one trailing parenthetical, and mine was not the one it accepts.
const displayValue = displayName.value.replace(/\s*\(Beta\)$/, "");
if (!displayValue.endsWith("Trigger")) { /* report */ }
(Beta) is stripped before the check. (Unofficial) is not. If you maintain an n8n community node, run npm run scan against your published package, not your working tree.
The rest of the release, briefly:
- Node 20 is out. It reached end of life on 30 April 2026, so
engines.nodemoved to>=22and CI runs on 22 and 24. To be accurate: this is a policy decision, not a technical necessity. The n8n 1.x line is still actively published and still permits Node 20.19. I dropped it because shipping against an unpatched runtime is a bad default. - Publishing moved to npm trusted publishing over OIDC. The token had expired, and instead of regenerating it I removed it. The workflow now mints a short lived, workflow scoped credential, provenance attestations generate automatically, and there is nothing left in the repository to rotate. One trap on the way: trusted publishing needs npm 11.5.1 or newer, and Node 22 ships npm 10.9.8, so the publish job moved to Node 24.
- The lint config now matches the scanner, including
package.json, which needs the TypeScript parser because those rules walk a TSESTree AST. Roughly a dozen rules had never run locally. I verified it by reintroducing each class of error and confirming the lint failed. - Dependencies aligned to the versions that actually matter:
@n8n/eslint-plugin-community-nodes0.27.0, the version the scanner itself pins, andn8n-workflow2.32.1, the exact version inside n8n 2.32.7. All fournpm auditfindings cleared. TypeScript 7 stays out, because it dropsmoduleResolution=node10and typescript-eslint does not load against it. - CI caught something I could not have caught locally. The lockfile, generated on macOS with npm 10.9.8, omitted optional binaries for other platforms. npm 11 on Node 24 is stricter and refuses
npm ci. Regenerated with npm 11, 463 entries added, verified npm 10 still accepts it.
Three commits, 26 files, 1922 insertions, 2229 deletions. The third one exists because on rereading I noticed the CHANGELOG claimed the new thresholds meant a regression would trip the gate, and it would not have, since CI ran npm test, which does not check thresholds. I made the sentence true rather than deleting it.
main 3516e51
npm 0.3.3 latest, published 2026-08-02T15:13:46Z
gitHead ae4a461 (matches the git tag exactly)
engines >=22
runtime dependencies: zero
provenance npm attestation + SLSA, generated automatically through OIDC
package 38 files, 170.7 KB unpacked
The package downloaded from npm, not my local build, also passed all 22 checks of the n8n loader contract.
What I am not claiming
That the node has no bugs. This session is the counterexample. It was verified, tested and covered, and it had six. It has now been interrogated seriously. That is a different sentence from "it is correct".
That coverage is meaningless. Two defects sat on fully covered lines, which makes coverage a floor rather than a verdict. It is still worth having.
That sx is broken. I did not test it individually on hardware. What I know is that the palette has no way to emit it without going through the builtin U, so it is not offered. That is not the same as "tested and it fails".
That every gate classification is empirical. The gates marked "transpile first" in the README are classified from the basis_gates the backend reports, which is a solid source, but only x, rx, rz, cz, reset, barrier and measure have been confirmed by jobs that completed.
That Node 20 could not run this. It could. I dropped it on policy.
Questions people ask
Why does my IBM Quantum job fail with "the instruction u is not supported" when my circuit has no u gate?
Almost certainly because it contains an identity gate. stdgates.inc defines id as U(0, 0, 0), IBM's parser expands it into the builtin U, and the target refuses U. The backend lists id in its own basis_gates, which is what makes this so confusing. Remove the identity gates. They are no ops, so the circuit is unchanged.
What does reason_code 1517 mean?
The circuit was not transpiled to the target's native gate set. Either transpile it with Qiskit against the real backend, or build it directly from the native gates the device reports.
Why does Gate Twirling fail my transpiled circuit?
Because Qiskit transpiles for Heron processors using parametrised rx, which IBM classifies as a fractional gate, and gate twirling does not support fractional gates. Turn it off. Dynamical Decoupling and Measurement Twirling have no such restriction.
Can I use rzz on IBM hardware through OpenQASM 3?
Not through the REST API as of August 2026. The backend lists rzz in basis_gates, but submitting it produces Error parsing OpenQASM program before it ever reaches the target. Verified twice on ibm_kingston, with a control circuit confirming the rest of the program was valid.
Do failed quantum jobs use up my monthly quota?
Yes, when the failure is your fault. IBM reports zero usage for a system error, but a user error or a manual cancellation is charged for whatever was consumed, including the roughly 2 second QPU preparation overhead. Five deliberately broken circuits cost me 10 seconds.
Can you build an ISA circuit without Qiskit?
For small circuits, yes. On a Heron processor the usable native set is x, rx, rz, cz, plus measure, reset and barrier. H equals rz(π/2), rx(π/2), rz(π/2), and CNOT equals H on the target, cz, H on the target. A Bell state built that way returned 97.3 percent correlation on ibm_marrakesh.
Why did a verified n8n community node fail the official scan?
Because the scanner downloads the source from the commit the provenance attests to and lints it with eslint-plugin-n8n-nodes-base, which many local configs do not load, and it lints package.json, which many do not cover. Run npm run scan against the published package.
The run in numbers
Run window: 2 August 2026, 13:38:45 to 14:22:19 UTC. Forty three minutes.
| jobs submitted | 28 |
| completed | 22 |
| failed, of which 5 were deliberate negative tests | 6 |
| QPU time consumed | 121 s |
on ibm_kingston | 25 |
on ibm_marrakesh | 3 |
on ibm_fez | 0, queried only |
| monthly Open plan allowance | 600 s |
The per job usage column sums to exactly 121, and that matches the change in reported instance usage across the campaign. Two independent sources, same number.
The jobs behind the claims in this post, so you can match them against the console:
| tag | job ID | backend | what it shows |
|---|---|---|---|
qa-idfix | d9nkqacsfqic73ar5o0g | ibm_kingston | the identity circuit completing after the fix |
qa-rzz-pi | d9nkvm4sfqic73ar5t5g | ibm_kingston | rzz at π, parse error |
qa-rzz-half | d9nkvmcsfqic73ar5t60 | ibm_kingston | rzz at π/2, parse error |
qa-rzz-control | d9nl060qs0bc73e3ifvg | ibm_kingston | same circuit without rzz, completed |
qa-bell-native | d9nktfoqs0bc73e3id5g | ibm_marrakesh | Bell state from native gates only |
qa-est-map | d9nkmbs60llc73ca7e90 | ibm_kingston | Estimator with a coefficient map |
qa-param | d9nkmkc60llc73ca7epg | ibm_kingston | parametrised rx at θ = π |
qa-bigpayload | d9nkn7oqs0bc73e3i53g | ibm_kingston | 64 bit register, BigInt decoding |
qa-ghz12-qiskit | d9nl2tk60llc73ca7v1g | ibm_kingston | first Qiskit transpiled run |
qa-ghz40 | d9nl3rssfqic73ar622g | ibm_kingston | Gate Twirling on fractional gates, failed |
qa-ghz40 | d9nl450qs0bc73e3ikm0 | ibm_kingston | same circuit, twirling off, completed |
qa-ghz80-max | d9nl56ssfqic73ar63fg | ibm_kingston | 80 qubits, depth 241, 45,000 shots |
The environment, if you want to reproduce any of it: n8n 2.32.7 on Node 22.23.1, with an isolated data folder outside ~/.n8n and telemetry disabled. Qiskit 2.2.3 with NumPy 2.0.2 in a separate virtual environment, used only for transpilation. IBM Quantum Platform in us-east, IBM-API-Version 2026-04-15, Open plan, Heron r2 processors at 156 qubits, with a ceiling of 100,000 shots per job.
A note on one number. The published CHANGELOG for 0.3.3 records 118 seconds of QPU time. That figure was written before the campaign finished. The number in this post is 121, and it is the one supported by both the per job usage column and the reported instance usage. I have left the CHANGELOG as published rather than editing history, so this note is here to explain the difference rather than hide it.
Where to find it
The node is on npm as n8n-nodes-ibm-quantum. It installs from the community nodes screen on self hosted n8n, and directly from the canvas on n8n Cloud because it is verified. Source, CHANGELOG and the full gate table are in the repository. It is unofficial and not affiliated with IBM.
Earlier chapters, if you want them: the launch and what the three nodes do, and the first end to end Bell state on real hardware, which covers the transpilation step most first attempts miss. IBM's documentation on workload usage is the source for the quota accounting above, and worth reading before you spend a monthly allowance.
What I keep turning over is that none of the six defects came from not knowing enough quantum computing. Every one came from trusting a layer. That green tests meant working behaviour. That a covered line meant a correct line. That a gate the vendor lists as native is a gate you can use. The hardware had no opinion about any of it. That is the whole value of running the thing for real.