Using Triggers and Probes
Drift cannot make your service publish a message, and it cannot see inside your service to confirm it consumed one. Triggers and probes are how you supply those two things.
A trigger causes your service to do something. A probe reports what happened. Drift runs both at the right moment in the verification sequence.
Triggers and probes apply to AsyncAPI operations only. They are declared per operation in your test case, and they are distinct from Lua Lifecycle Hooks, which respond to Drift engine events across a whole run.
Choosing the Right Hook
| After Drift subscribes, before it captures | Cause your service to publish the message Drift is waiting for |
|
| After Drift publishes | Query a side effect and return JSON evidence |
|
| Instead of Drift publishing | Publish the message yourself, when the broker has no Drift transport plugin |
|
Hook Runtimes
Every hook uses the same shape; executable-type selects the runtime.
Every hook uses the same shape; executable-type selects the runtime.
External Commands
Commands are the primary mechanism. They let you reuse the scripts, test frameworks, Make targets, and helper CLIs you already have, without rewriting them in Lua.
trigger:
executable-type: command
value: python3
parameters:
args:
- ./hooks/trigger-send.py
- --correlation-id
- ${parameters.correlation-id}
- --order-id
- ${parameters.orderId}
timeout-ms: 2000Field | Required | Purpose |
|---|---|---|
| Yes |
|
| Yes | The executable to run |
| No | Arguments, as a list |
| No | How long to wait for the command. Falls back to the operation |
Paths are resolved relative to the test case file, so a test file in drift/ refers to a hook in hooks/ as ../hooks/trigger-send.py.
A hook that exits non-zero fails the operation. Anything the command writes to standard error appears in the Drift output, so write diagnostics there.
Any Drift expression works inside value and parameters.args, which is how you keep the hook and the test case using the same correlation ID.
Lua Functions
Use Lua for lightweight inline logic where a separate script would be overhead.
probe: executable-type: lua-function value: check_inventory_state timeout-ms: 3000
The function must be exported from a Lua script declared in your sources block:
sources:
- name: async-order
path: ../asyncapi/order.asyncapi.yaml
- name: hooks
path: ../hooks/probe.lua-- hooks/probe.lua
local function check_inventory_state(correlation_id)
-- inspect state and return a table
return {
status = "processed",
correlationId = correlation_id
}
end
return {
exported_functions = {
check_inventory_state = check_inventory_state
}
}See Lua Scripting for the scripting model.
Writing a Trigger
A trigger makes your service publish the message Drift is waiting for. Drift subscribes first, then runs the trigger, then waits. Subscribing first means a fast service cannot publish before Drift is listening.
sequenceDiagram
autonumber
participant D as Drift
participant B as Broker
participant S as Your service
D->>B: Subscribe to the channel
Note over D,B: Always first
D->>S: Run the trigger hook
S->>B: Publish message
B-->>D: Deliver matching message
D->>D: ValidateA trigger can do whatever causes the behavior: call an HTTP endpoint on your service, run a CLI command, insert a database row, or run a one-shot script that publishes directly.
# Call an endpoint that causes the service to emit an event
trigger:
executable-type: command
value: curl
parameters:
args:
- "-X"
- "POST"
- "http://localhost:8080/orders"
- "-H"
- "content-type: application/json"
- "-H"
- "x-correlation-id: ${parameters.correlation-id}"
- "-d"
- '{"customerId":"CUST-9"}'
timeout-ms: 3000The trigger must propagate the correlation ID. Drift filters captured messages by correlation ID. If the message your service publishes carries a different one, Drift will not recognize it and the operation fails with a capture timeout.
Triggers do not return data. Drift only checks the exit code. Everything Drift validates comes from the captured message.
Writing a Probe
A probe answers the question "did the service handle the message correctly?" for async-inject, where there is nothing on the broker for Drift to capture.
The probe must write JSON to standard output. Drift parses that JSON and compares it against your expected block.
#!/usr/bin/env python3
"""Probe: report the state written by the service for one correlation ID."""
import argparse
import json
import pathlib
import sys
parser = argparse.ArgumentParser()
parser.add_argument("--correlation-id", required=True)
args = parser.parse_args()
state_file = pathlib.Path("state") / f"{args.correlation_id}.json"
if not state_file.exists():
print(f"no state recorded for {args.correlation_id}", file=sys.stderr)
sys.exit(1)
print(state_file.read_text())probe:
executable-type: command
value: python3
parameters:
args:
- ../hooks/probe-receive.py
- --correlation-id
- ${parameters.correlation-id}
timeout-ms: 3000
expected:
status: processed
correlationId: ${parameters.correlation-id}
sku: ${parameters.payload.sku} Drift compares the probe output against expected. It does not schema-validate the output, because the probe result is not described in your AsyncAPI document.
Handle timing in the probe. Drift runs the probe as soon as the publish completes, which may be before your service has finished. If processing is not immediate, poll inside the probe rather than lengthening the operation timeout:
import time
for _ in range(20):
if state_file.exists():
print(state_file.read_text())
sys.exit(0)
time.sleep(0.1)
print("timed out waiting for state", file=sys.stderr)
sys.exit(1)Reset state between runs. A probe that reads a file left behind by an earlier run will report a false pass. Clear state in an operation:started Lua hook:
-- hooks/reset-state.lua
return {
event_handlers = {
["operation:started"] = function(event, data)
os.execute("rm -f state/*.json")
end
}
}Using a Probe Topic Instead of a Probe
When your test harness can publish its result to a topic, a probe topic is more reliable than a probe command. Drift subscribes before it injects, so there is no window in which the result can be missed.
Set probe-topic and Drift switches to async-inject-capture:
operations:
ReceiveInventoryAdjusted_InjectCapture:
target: async-receive:receiveInventoryAdjusted:inventoryAdjusted
parameters:
probe-topic: drift.examples.inventory-adjusted-probe
timeout-ms: 5000
correlation-id: receive-inventory-adjusted-capture-001
payload:
commandType: inventory.adjusted
sku: SKU-RED-CHAIR
quantityDelta: -2
reason: reservation
trigger:
executable-type: command
value: python3
parameters:
args:
- ../hooks/trigger-inject-capture.py
- --correlation-id
- ${parameters.correlation-id}
- --probe-topic
- ${parameters.probe-topic}
timeout-ms: 8000
expected:
headers:
correlation-id: ${parameters.correlation-id}
payload:
status: processed
sku: ${parameters.payload.sku}
quantityDelta: ${parameters.payload.quantityDelta}sequenceDiagram
autonumber
participant D as Drift
participant B as Broker
participant H as Your test harness
D->>B: Subscribe to the probe topic
D->>B: Publish to the input channel
opt Trigger configured
D->>H: Run the trigger hook
end
B-->>H: Deliver the injected message
H->>B: Publish the result to the probe topic
B-->>D: Deliver the probe topic message
D->>D: Compare against expectationsTwo constraints apply:
The probe topic must be on the same broker as the injection target. The same transport plugin handles both.
Probe topic messages must be JSON. The probe topic is test infrastructure and is not described in your AsyncAPI document, so Drift cannot derive a schema for it. Drift parses the message as
application/jsonand compares it againstexpectedwithout schema validation. If you need a different wire format, use a probe command instead.
Publishing the Message Yourself
For async-inject, publish-trigger replaces Drift's publish step with your own command. Use it when the broker has no Drift transport plugin.
publish-trigger:
executable-type: command
value: ./scripts/publish-to-legacy-bus.sh
parameters:
args: ["--correlation-id", "${parameters.correlation-id}"]
timeout-ms: 5000The hook receives the execution plan and must return JSON, usually including a correlation identifier. The probe hook can then reference the publish result with ${result:publish.<field>}.
publish-trigger applies to async-inject only. async-inject-capture and async-request-reply require Drift's own publish, because publishing is coupled to an active subscription.
Troubleshooting
Symptom | Likely cause |
|---|---|
Capture timeout, trigger succeeded | The published message carries a different correlation ID, or went to a different channel address than the document declares |
Capture timeout, no trigger output | The trigger command path is wrong. Paths are relative to the test case file. |
Probe reports success on a broken service | State from an earlier run was not cleared |
Probe fails intermittently | The probe runs before processing finishes. Poll inside the probe. |
Hook timed out | The hook's own timeout-ms is too low for the work it does |
Probe output rejected | The probe wrote non-JSON to standard output. Send logs to standard error. |
See Debugging Test Cases.