Defining Custom Gates in OpenQASM 3
How to define reusable custom gates in OpenQASM 3 using the gate keyword, angle parameters, and inline gate modifiers.
Circuit diagrams
Why Custom Gate Definitions Matter
Quantum hardware supports a small set of native gates, typically between two and five. IBM’s superconducting processors, for example, natively support ECR (echoed cross-resonance), RZ, SX, and X. Google’s Sycamore chip uses SYC (the Sycamore gate) along with single-qubit rotations. Every gate you write in a quantum program must eventually be decomposed into whichever native gates your target hardware provides.
This decomposition process is the compiler’s job. But as a programmer, you need a way to express higher-level operations cleanly. You do not want to write out 15 native gates every time you need a Toffoli.
OpenQASM 3’s gate keyword solves this problem. It lets you define reusable unitary operations composed of other gates, exactly the same way functions in classical programming let you name and reuse a block of code. You define a gate once, then call it by name wherever you need it. The compiler handles the decomposition to native operations when it targets specific hardware.
There is one critical constraint: gate definitions in QASM 3 can only contain unitary operations. You cannot put measurement, reset, or classical control flow inside a gate block. These are irreversible operations, and gates must be reversible (unitary). For non-unitary operations that include measurement or classical logic, QASM 3 provides def subroutines, which are covered later in this tutorial.
The U Gate: The Single-Qubit Primitive
Every gate in OpenQASM 3 ultimately decomposes into the built-in primitives U (the general single-qubit unitary) and CX (CNOT). Understanding U is essential because it is the foundation that every custom single-qubit gate compiles down to.
U(theta, phi, lambda) is the general single-qubit rotation gate, parameterized by three Euler angles. Its matrix representation is:
U(θ, φ, λ) = | cos(θ/2) -e^(iλ) sin(θ/2) |
| e^(iφ) sin(θ/2) e^(i(φ+λ)) cos(θ/2) |
Every single-qubit unitary can be expressed as U(theta, phi, lambda) for some choice of angles (up to a global phase). Here are the most common gates and their U-decompositions:
| Gate | U Decomposition | Notes |
|---|---|---|
| H (Hadamard) | U(pi/2, 0, pi) | Creates equal superposition |
| X (Pauli-X) | U(pi, 0, pi) | Bit flip |
| Y (Pauli-Y) | U(pi, pi/2, pi/2) | Bit and phase flip |
| Z (Pauli-Z) | U(0, 0, pi) | Phase flip |
| S | U(0, 0, pi/2) | sqrt(Z) |
| T | U(0, 0, pi/4) | Fourth root of Z |
| RX(theta) | U(theta, -pi/2, pi/2) | Rotation around X axis |
| RY(theta) | U(theta, 0, 0) | Rotation around Y axis |
| RZ(phi) | U(0, 0, phi) | Rotation around Z axis (up to global phase) |
When a compiler decomposes your custom gate for hardware, it converts each single-qubit operation into a U call (or the hardware-native equivalent) and each multi-qubit interaction into CX gates plus single-qubit rotations.
You can verify any of these decompositions yourself:
OPENQASM 3.0;
// Hadamard defined via U
gate my_h q {
U(pi/2, 0, pi) q;
}
// X gate defined via U
gate my_x q {
U(pi, 0, pi) q;
}
// T gate defined via U
gate my_t q {
U(0, 0, pi/4) q;
}
qubit q;
my_h q; // identical to h q after including stdgates.inc
Basic Gate Definitions
A gate definition takes a name, optional angle parameters, and qubit arguments:
OPENQASM 3.0;
include "stdgates.inc";
// Define a Hadamard gate manually (already in stdgates, shown for illustration)
gate my_h q {
U(pi/2, 0, pi) q;
}
// Define a SWAP gate from three CNOTs
gate my_swap a, b {
cx a, b;
cx b, a;
cx a, b;
}
qubit[2] q;
my_h q[0];
my_swap q[0], q[1];
The qubit arguments (a, b) are positional; when you call my_swap q[0], q[1], the first qubit maps to a and the second to b.
Why SWAP Requires Three CNOTs
The three-CNOT decomposition of SWAP is not arbitrary. It follows from a specific algebraic identity. Write CNOT(a,b) for the CNOT gate with control a and target b. The SWAP gate satisfies:
SWAP = CNOT(a,b) · CNOT(b,a) · CNOT(a,b)
To see why this works, trace through the action on computational basis states. Start with |a, b>:
CNOT(a,b): flipsbifa=1, giving|a, a XOR b>CNOT(b,a): flipsaif(a XOR b)=1, giving|a XOR (a XOR b), a XOR b>=|b, a XOR b>CNOT(a,b): flips the second qubit ifb=1, giving|b, (a XOR b) XOR b>=|b, a>
The qubits have been swapped. This decomposition is optimal: any SWAP implementation requires at least 3 two-qubit gates. More generally, any two-qubit unitary can be compiled from at most 3 CNOTs and single-qubit rotations (this is a proven lower bound result in quantum compilation theory).
Parameterized Gates
Gates can accept angle parameters in addition to qubit arguments. Parameters are listed in parentheses before the qubit arguments:
OPENQASM 3.0;
include "stdgates.inc";
// Parameterized rotation around an arbitrary axis in the XZ plane
gate rxz(theta, phi) q {
rz(phi) q;
rx(theta) q;
rz(-phi) q;
}
// A controlled-phase gate with a variable angle
gate cphase(lambda) a, b {
rz(lambda/2) a;
cx a, b;
rz(-lambda/2) b;
cx a, b;
rz(lambda/2) b;
}
qubit[2] q;
rxz(pi/4, pi/3) q[0];
cphase(pi/2) q[0], q[1];
Parameters are always angles (real-valued). You can use arithmetic expressions involving pi and the parameter names inside the gate body.
Controlled Rotations: CRX
One commonly needed parameterized gate is the controlled-RX rotation, CRX(theta). This gate applies an X-rotation of angle theta to the target qubit only when the control qubit is |1>. Controlled rotations appear frequently in quantum phase estimation circuits, variational algorithms, and quantum Fourier transform implementations.
OPENQASM 3.0;
include "stdgates.inc";
// Controlled-RX gate decomposition
gate crx(theta) c, t {
rz(pi/2) t;
cx c, t;
ry(-theta/2) t;
cx c, t;
ry(theta/2) t;
rz(-pi/2) t;
}
qubit[2] q;
h q[0]; // Put control in superposition
crx(pi/4) q[0], q[1]; // Controlled X-rotation by pi/4
The decomposition uses two CNOT gates and single-qubit rotations. The pattern is general: any controlled single-qubit rotation can be decomposed into two CNOTs and three single-qubit rotations, following the ABC decomposition where A · B = I and A · Z · B · Z = U for the desired unitary U.
Nested Gate Definitions
Custom gates can call other custom gates, allowing you to build complex operations from simpler layers:
OPENQASM 3.0;
include "stdgates.inc";
// Toffoli (CCX) decomposition from single-qubit and CNOT gates
gate my_toffoli a, b, c {
h c;
cx b, c;
tdg c;
cx a, c;
t c;
cx b, c;
tdg c;
cx a, c;
t b;
t c;
h c;
cx a, b;
t a;
tdg b;
cx a, b;
}
qubit[3] q;
x q[0];
x q[1];
my_toffoli q[0], q[1], q[2]; // q[2] should flip to |1>
This is the standard decomposition of the Toffoli gate into 6 CNOT and several single-qubit gates. Defining it as a named gate makes the program more readable and lets you reuse it without repeating the full decomposition.
Why This Decomposition Works
The Toffoli gate flips the target qubit c if and only if both control qubits a and b are |1>. Implementing this requires creating a phase relationship that distinguishes the |110> and |111> states from all others.
The T and T-dagger gates are the key ingredients. Each T gate applies a phase of pi/4, and the CNOT gates route these phases so they accumulate correctly: the |11> state on the controls picks up a net phase of pi (which, combined with the Hadamard sandwich on the target, produces a bit flip), while all other control states see canceling phases.
The 6-CNOT count in this decomposition is known to be optimal for Toffoli when restricted to CNOT as the only two-qubit gate. There exist decompositions using fewer two-qubit gates if you allow other entangling gates (for example, 3 controlled-Z gates suffice), but since CNOT is the standard two-qubit primitive in QASM, this 6-CNOT version is the practical choice.
Gate Modifiers
OpenQASM 3 provides three inline gate modifiers that transform existing gates without requiring a separate definition:
inv @ (Inverse)
Applies the inverse (adjoint) of a gate:
OPENQASM 3.0;
include "stdgates.inc";
qubit q;
s q; // Apply S gate
inv @ s q; // Apply S-dagger (inverse of S)
// Net effect: identity
The compiler reverses the gate sequence and replaces each gate with its adjoint. For a composite gate, inv @ reverses the order of the internal gates and inverts each one.
Practical Use: Uncomputation with inv @
One of the most common patterns in quantum algorithms is “uncomputation,” where you apply a gate sequence to entangle an ancilla qubit with your data, perform some operation, and then apply the inverse of the original sequence to disentangle (reset) the ancilla back to |0>. This pattern appears in quantum phase estimation, Grover’s oracle constructions, and any algorithm that uses temporary ancilla qubits.
Without inv @, you would have to manually write out the reversed gate sequence with each gate replaced by its adjoint. For a complex multi-gate definition, this is tedious and error-prone. The inv @ modifier handles it automatically.
OPENQASM 3.0;
include "stdgates.inc";
// A custom entangling operation
gate entangle_ancilla a, data {
h a;
cx a, data;
t a;
cx data, a;
}
qubit data;
qubit ancilla;
// Step 1: Entangle the ancilla with data
entangle_ancilla ancilla, data;
// Step 2: Perform some operation that uses the entangled state
// (other gates would go here)
rz(pi/4) data;
// Step 3: Uncompute the ancilla by applying the inverse
inv @ entangle_ancilla ancilla, data;
// The ancilla is now back in |0>, disentangled from data
The compiler expands inv @ entangle_ancilla into the reversed sequence: inv @ cx data, a; inv @ t a; inv @ cx a, data; inv @ h a;, which simplifies to cx data, a; tdg a; cx a, data; h a; (since CX and H are self-inverse, and the inverse of T is T-dagger).
ctrl @ (Controlled)
Adds a control qubit to any gate:
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
// Controlled-S gate (equivalent to the CS gate)
ctrl @ s q[0], q[1];
// Doubly-controlled X (Toffoli) using nested ctrl
ctrl @ ctrl @ x q[0], q[1], q[2];
// Controlled-SWAP (Fredkin gate)
ctrl @ swap q[0], q[1], q[2];
The first qubit argument becomes the control; the remaining arguments are passed to the target gate. You can nest ctrl @ to add multiple controls.
Practical Use: Doubly-Controlled Rotations
Quantum phase estimation and many variational algorithms require multiply-controlled rotations. Instead of writing a full decomposition by hand, you can nest ctrl @ modifiers:
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
// Doubly-controlled RZ rotation
// q[0] and q[1] are controls, q[2] is the target
ctrl @ ctrl @ rz(pi/8) q[0], q[1], q[2];
The compiler decomposes ctrl @ ctrl @ rz(pi/8) into a sequence of CNOTs and single-qubit rotations. For a doubly-controlled single-qubit gate, the standard decomposition requires roughly 14 CNOTs, though optimizing compilers can sometimes reduce this count depending on the specific gate.
You can also mix ctrl @ with custom gates:
OPENQASM 3.0;
include "stdgates.inc";
gate my_rotation(theta) q {
rx(theta) q;
rz(theta/2) q;
}
qubit[3] q;
// Controlled version of the custom rotation
ctrl @ my_rotation(pi/4) q[0], q[1];
// Doubly-controlled version
ctrl @ ctrl @ my_rotation(pi/4) q[0], q[1], q[2];
This composability is one of the most powerful features of OpenQASM 3’s modifier system. You define the base operation once, and the compiler generates the controlled version for you.
pow(k) @ (Power)
Applies a gate k times, or more precisely, raises the gate’s unitary matrix to the power k:
OPENQASM 3.0;
include "stdgates.inc";
qubit q;
// Square root of X: pow(0.5) @ x is equivalent to the SX gate
pow(0.5) @ x q;
// T gate is the fourth root of Z
pow(0.25) @ z q;
Fractional powers are particularly useful in phase estimation circuits and for constructing controlled rotations at specific angles.
Combining Modifiers
Modifiers can be chained. They apply from right to left (closest to the gate name applies first):
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
// Controlled inverse of S
ctrl @ inv @ s q[0], q[1];
// Inverse of controlled-Z
inv @ ctrl @ z q[0], q[1];
// Controlled square-root of X
ctrl @ pow(0.5) @ x q[0], q[1];
Gate Definitions vs Subroutines
OpenQASM 3 provides two distinct mechanisms for code reuse, and choosing the right one matters.
gate definitions declare unitary operations. The body of a gate block can only contain applications of other gates. No classical variables, no measurement, no reset, no conditional logic. Because the operation is guaranteed to be unitary, the compiler can safely compute its inverse (via inv @), add controls (via ctrl @), and raise it to a power (via pow @).
// This is a gate: purely unitary, supports modifiers
gate my_gate a, b {
h a;
cx a, b;
rz(pi/4) b;
}
def subroutines declare general-purpose callable blocks. They can contain measurement, reset, classical variables, loops, conditionals, and any other QASM 3 statement. However, because subroutines can perform irreversible operations, you cannot apply inv @, ctrl @, or pow @ to a subroutine call.
// This is a subroutine: can measure, reset, branch on classical values
def measure_and_reset(qubit q) -> bit {
bit result;
result = measure q;
reset q;
return result;
}
qubit q;
bit b;
h q;
b = measure_and_reset(q); // q is now |0>, b holds the measurement result
Use gate when your operation is purely unitary and you want modifier support. Use def when you need measurement, reset, or classical control flow.
Common Mistakes
Trying to Measure Inside a Gate Definition
Gate bodies can only contain unitary operations. Placing a measure or reset inside a gate block produces a compile error:
// WRONG: this will not compile
gate bad_gate q {
h q;
measure q; // Error: measurement is not a unitary operation
}
// CORRECT: use a def subroutine instead
def measure_in_h_basis(qubit q) -> bit {
h q;
return measure q;
}
Confusing Positional Qubit Arguments
Gate arguments are positional. For a symmetric gate like SWAP, the order does not matter because SWAP(a, b) = SWAP(b, a). But for asymmetric gates, argument order changes the operation:
OPENQASM 3.0;
include "stdgates.inc";
// This gate is NOT symmetric: a is control, b is target
gate my_controlled_op a, b {
cx a, b;
rz(pi/4) b;
}
qubit[2] q;
// These two calls produce DIFFERENT results:
my_controlled_op q[0], q[1]; // q[0] controls, q[1] is target
my_controlled_op q[1], q[0]; // q[1] controls, q[0] is target
Always name your qubit parameters descriptively (e.g., control, target instead of a, b) to make the intended usage clear.
Forgetting include "stdgates.inc"
The standard gates (h, cx, cz, s, t, rx, ry, rz, and others) are not built into the language. They are defined in the stdgates.inc library file. If you omit the include directive, these gates are undefined and any reference to them produces an error:
// WRONG: h is not defined without the include
OPENQASM 3.0;
gate my_gate q {
h q; // Error: gate 'h' is not defined
}
// CORRECT: include the standard library
OPENQASM 3.0;
include "stdgates.inc";
gate my_gate q {
h q; // Now this works
}
The only gates available without stdgates.inc are the built-in primitives U and CX.
Nested ctrl @ for Multi-Controlled Gates
Writing ctrl @ ctrl @ x to get a Toffoli gate is valid QASM 3 syntax. Some programmers assume they need to write a separate gate definition for multi-controlled operations, but the nested modifier approach is cleaner and lets the compiler choose the optimal decomposition for the target hardware:
OPENQASM 3.0;
include "stdgates.inc";
qubit[4] q;
// All of these are valid:
ctrl @ ctrl @ x q[0], q[1], q[2]; // Toffoli
ctrl @ ctrl @ ctrl @ x q[0], q[1], q[2], q[3]; // 3-controlled X (C3X)
ctrl @ ctrl @ rz(pi/4) q[0], q[1], q[2]; // Doubly-controlled RZ
The compiler recursively decomposes each ctrl @ layer. Writing a manual decomposition is only necessary when you need a specific decomposition strategy (for example, one that minimizes T-gate count for error-corrected circuits).
Practical Notes
- Gate definitions in OpenQASM 3 describe unitary operations only. They cannot contain measurements, resets, or classical logic. For non-unitary operations, use
defsubroutines instead. - The
stdgates.incfile defines the standard gate library (h, x, y, z, cx, cz, s, t, rx, ry, rz, and others). Always include it unless you want to define everything from scratch. - Gate modifiers are syntactic sugar handled by the compiler. On hardware,
ctrl @ swill be decomposed into native gates just like any other multi-qubit operation. - Not all hardware backends support arbitrary gate modifier combinations. When using these in practice, verify that your target backend’s compiler can handle the decomposition.
- When designing custom gates for NISQ hardware, keep decomposition depth in mind. Every additional CNOT in the decomposition adds noise. Prefer decompositions that minimize two-qubit gate count.
- Custom gate definitions do not carry optimization hints. The compiler is free to re-decompose and optimize your gate body. If you need an exact gate sequence on hardware (for benchmarking or calibration), consult your provider’s documentation for how to lock the decomposition.
Was this tutorial helpful?