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

trigger

After Drift subscribes, before it captures

Cause your service to publish the message Drift is waiting for

async-observe

async-inject-capture

probe

After Drift publishes

Query a side effect and return JSON evidence

async-inject

publish-trigger

Instead of Drift publishing

Publish the message yourself, when the broker has no Drift transport plugin

async-inject

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: 2000

Field

Required

Purpose

executable-type

Yes

command

value

Yes

The executable to run

parameters.args

No

Arguments, as a list

timeout-ms

No

How long to wait for the command. Falls back to the operation timeout-ms.

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: Validate

A 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: 3000

The 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 expectations

Two 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/json and compares it against expected without 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: 5000

The 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.

See Also

Publication date: