Hi @cnada!

There is indeed a way of doing this, but it requires ‘dropping down’ to a lower level of abstraction, and working with `tapes`

. These are the internal data structures that represents the variational circuit, and are created internally whenever a QNode is executed.

You can create a tape manually:

```
with qml.tape.QuantumTape() as tape:
qml.RX(0.5, wires=0)
qml.CNOT(wires=[0, 1])
qml.expval(qml.PauliZ(1))
```

or you can extract it from a QNode:

```
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(1))
# first we 'construct' the QNode with a particular
# parameter value
x = np.array(0.6, requires_grad=True)
# pass a list of QNode arguments and
# a dict of keyword arguments
circuit.construct([x], {})
tape = circuit.tape
```

Now, we can apply `qml.gradients.param_shift`

*directly to the tape*. Unlike when we apply this gradient transform to a QNode, *no quantum evaluation will occur*.

Instead, we get returned a list of **gradient tapes** (representing each shifted argument in the parameter-shift rule) and a post-processing function.

```
>>> g_tapes, fn = qml.gradients.param_shift(tape)
>>> len(g_tapes)
2
>>> for t in g_tapes:
... print(t.draw())
0: ──RX(2.17)──╭C──┤
1: ────────────╰X──┤ ⟨Z⟩
0: ──RX(-0.971)──╭C──┤
1: ──────────────╰X──┤ ⟨Z⟩
```

We can now evaluate these ‘gradient tapes’ using `qml.execute`

:

```
>>> results = qml.execute(g_tapes, dev, None)
>>> results
[array([-0.56464247]), array([0.56464247])]
```

and compute the gradient by ‘post-processing’ them with the generated function `fn`

:

```
>>> fn(results)
array([[-0.56464247]])
```