← Files Zuora Coding AgentARCHIVED FILE
references/workflow-events.md
11.5 KB · Oct 4, 2026 · 12:30 UTC
# Zuora Workflow events reference
How `event_trigger` workflows are wired, what event names are accepted, and which Zuora REST endpoints inform the composer's choices.
This document is the authoritative ground truth for every key under `workflow.parameters.event_triggers` and `workflow.parameters.event_parameters`. The structured catalog lives in `workflow-enums.json` (`standard_events`, `event_parameters_schema`); this Markdown explains the *why* and shows derivation patterns.
## How an event-triggered workflow is wired
When `workflow.event_trigger: true`:
1. Zuora's Kafka consumer dispatches matching `BusinessEvent` records to all registered workflows. The match is name-based: `event_triggers[]` must contain the event's `name`.
2. For each matched event, Rails extracts the values listed in `event_parameters[*].params[]` and binds them into the workflow instance's `Data` scope under the configured `object` and `key`.
3. Tasks then access these values via Liquid: `{{ Data.<Object>.<Key> }}`.
So `event_parameters` is a contract: it tells Rails which event payload fields to pull off the wire and where to place them in `Data` so downstream tasks can read them.
## Where the standard event names come from
The hard-coded list lives in `app/javascript/components/WorkflowDefinitionForm.js` L150-219 (`defaultEvents`). It's the authoritative inventory of events that work out of the box with no custom registration. The full list is mirrored in `workflow-enums.json` -> `standard_events.events`.
Highlights for common use cases:
| User intent | Canonical event name | baseObject |
|---|---|---|
| "After a bill run completes" | `BillingRunCompletion` | `BillingRun` |
| "After an invoice is posted" | `InvoicePosted` | `Invoice` |
| "After a payment is processed" | `PaymentProcessed` | `Payment` |
| "After a credit memo is posted" | `CreditMemoPosted` | `CreditMemo` |
| "After a payment run finishes" | `PaymentRunCompletion` | `Payment` |
| "After a journal run finishes" | `JournalRunCompletion` | `JournalRun` |
| "Subscription renewal coming up" | `UpcomingRenewal` | `Subscription` |
| "Payment method expiring" | `PaymentMethodExpiration` | `PaymentMethod` |
### Common name corrections
Natural-language requests rarely match canonical names exactly. Always check `workflow-enums.json` -> `standard_events.$canonical_name_corrections` before emitting:
| User said | Correct value |
|---|---|
| "BillRunCompleted" | `BillingRunCompletion` |
| "BillRunCompletedSuccess" | `BillingRunCompletion` |
| "BillingRunCompleted" | `BillingRunCompletion` |
| "InvoiceCreated" | `InvoicePosted` |
| "PaymentReceived" | `PaymentProcessed` |
If the user's event name does not appear in either the standard catalog or the corrections table, treat it as a **custom event** (see below).
## Two REST endpoints fed into the picker
The settings React component (`WorkflowSettingsForm`) calls two endpoints to populate the event UI. Both are reachable from the agent via either a direct `curl` call (credentials live in `~/.claude/settings.json -> env` as `ZUORA_BASE_URL`, `ZUORA_CLIENT_ID`, `ZUORA_CLIENT_SECRET`, and Claude Code injects them into every `Bash` invocation) or `mcp__zuora-mcp__ask_zuora` as a fallback. One-time OAuth exchange per session:
```bash
ACCESS_TOKEN=$(curl -sS -X POST "$ZUORA_BASE_URL/oauth/token" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=$ZUORA_CLIENT_ID" \
--data-urlencode "client_secret=$ZUORA_CLIENT_SECRET" \
| jq -r '.access_token')
```
### 1. List available events
```bash
curl -sS "$ZUORA_BASE_URL/events/event-triggers" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.data[] | .eventType.name'
curl -sS "$ZUORA_BASE_URL/events/scheduled-events" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.data[] | .name'
```
Both are paginated (cursor in `body.next`). The settings UI concatenates them with the hard-coded `defaultEvents` and presents the combined list in alphabetical order.
Reference: `app/models/zuora_connect/app_instance.rb#get_custom_event_triggers` L1500-1525.
### 2. List available fields for one event
```bash
curl -sS "$ZUORA_BASE_URL/notifications/email-templates/info/selections?category=<category>" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq
```
Where `<category>` derives from the event metadata:
- If `event.id.length < 5` chars (i.e., a standard event id like `1410`): `category = event.id`.
- Otherwise (custom events): `category = ${event.namespace}:${event.name}` (e.g., `user.notification:MyCustomEvent`).
The response is a hash of object-name -> array-of-field-paths. The settings UI flattens these into `<...>` placeholder strings (e.g., `<BillingRun.Id>`, `<BillingRun.PostedDate>`) for the `value` field of `event_parameters[*].params[*]`.
Reference: `app/models/zuora_connect/app_instance.rb#get_custom_event_fields` L1452-1494 and `WorkflowSettingsForm.js` L383-401.
#### Field filtering rules
After fetching field options for an event:
- For **Standard** events (those in the `defaultEvents` list): exclude any returned field containing `DataSource`.
- For **Custom** events: include only fields containing `DataSource` OR matching `.${selBillEvent.baseObject}.`.
These rules come from `WorkflowSettingsForm.js` L388-400.
## Custom events
If the user references an event name not in the standard catalog and not in the corrections table, treat the name as a tenant-custom event candidate. The workflow envelope is still event-triggered: set `workflow.event_trigger: true`, use the exact registered custom event `name` in `parameters.event_triggers[]`, and create a matching `parameters.event_parameters[*].eventName` entry. The workflow will not trigger until the event is registered. Three options:
1. **Manual registration** (recommended for one-off setups): Settings -> Notifications -> Custom Events in the Zuora UI.
2. **API registration** via `POST /events/event-triggers` with payload:
```json
{
"active": true,
"baseObject": "Invoice",
"condition": "Invoice.Status == 'Posted'",
"eventType": {
"name": "MyCustomInvoiceEvent",
"displayName": "My Custom Invoice Event",
"description": "Triggered when an invoice is posted with custom condition"
}
}
```
Reference: `app/controllers/app_instance_controller.rb#create_custom_event` L68-81 and `app_instance.rb#create_custom_event_trigger` L1419-1450.
3. **Switch to a callout trigger** (often simpler): set `callout_trigger: true` instead of `event_trigger: true` and configure the corresponding standard Notification (Settings -> Notifications) to send a callout to the workflow's URL. This avoids needing to register a custom event.
## Composer recipe for an event-triggered workflow
For each event the user wants to react to:
1. **Resolve the canonical or registered custom name.** Look up the user's intent in `standard_events.$canonical_name_corrections`, then in `standard_events.events`. If no match, treat the user-provided string as a custom event name and follow "Custom events" above; do not disable the event trigger solely because the name is tenant-custom.
2. **Add to `parameters.event_triggers[]`.** Append the canonical standard name or exact registered custom event name string. Multiple events can share one workflow.
3. **Build the matching `parameters.event_parameters[*]` entry.** Shape:
```json
{
"eventName": "<canonical name>",
"params": [
{ "object": "<baseObject>", "key": "Id", "value": "<<baseObject>.Id>" }
]
}
```
Add one `params[]` row per field that downstream tasks reference. For most workflows, binding `Id` is sufficient because subsequent `Query` / `Export` tasks use it in `where_clause`.
4. **Verify task references match.** Every `{{ Data.<Object>.<Key> }}` in downstream tasks must correspond to an `event_parameters[*].params[]` row with matching `object` and `key`.
### Worked example: react to bill run completion
User's natural-language request: "Run after a bill run completes; query the invoices in that run."
```json
{
"ondemand_trigger": false,
"callout_trigger": false,
"scheduled_trigger": false,
"event_trigger": true,
"parameters": {
"fields": [],
"entity_name": null,
"entity_id": null,
"skipping_check": "db",
"file_encryption": "false",
"secure_error_msgs": "false",
"show_run_prompt": null,
"callout_response": "workflow instance",
"event_triggers": ["BillingRunCompletion"],
"event_parameters": [
{
"eventName": "BillingRunCompletion",
"params": [
{ "object": "BillingRun", "key": "Id", "value": "<BillingRun.Id>" }
]
}
]
}
}
```
A downstream `Export` task can then use `where_clause: "BillRunId = '{{ Data.BillingRun.Id }}'"`.
**Provenance note.** `<BillingRun.Id>` is not a hard-coded special token; it is a merge-field string that the UI picker obtains from `GET /notifications/email-templates/info/selections?category=<category>` (see `get_custom_event_fields` in `app_instance.rb`). Before emitting this `value`, confirm the field via `curl` against the Notifications API (credentials are in `~/.claude/settings.json -> env`) or via `mcp__zuora-mcp__ask_zuora` — whichever channel the session has access to. Otherwise accept the linter's `W179` warning that the token is statically unverifiable. The linter also cross-checks that `BaseObject` (`BillingRun` here) matches the event's declared `baseObject` (`$event_base_objects.events["BillingRunCompletion"] = "BillingRun"`); a mismatch raises `E177`.
Similarly, the downstream `Export.parameters.where_clause` references `Invoice.BillRunId`. Confirm the column via `curl $ZUORA_BASE_URL/v1/describe/Invoice` (or MCP `ask_zuora`) before emission; it is also carried in the fallback `references/zuora-standard-fields.json` under `Invoice.fields` so lint stays quiet if neither channel is reachable.
## Pitfalls the linter catches
| Code | Rule |
|---|---|
| `E007` | `event_trigger == true` but `parameters.event_triggers[]` is empty or missing |
| `E122` | `event_trigger == true` but `parameters.event_parameters[]` is empty, missing, or has at least one entry whose `eventName` is not in `event_triggers[]` |
| `E120` | `parameters.event_parameters[*]` shape is wrong (missing `eventName`, missing/non-array `params`, or `params[*]` missing `object`/`key`/`value`) |
| `W121` | `event_triggers[]` contains a name that is not in the standard catalog and the MCP preflight (`GET /events/event-triggers`) did not confirm registration |
| `W123` | `event_triggers[]` or `event_parameters[]` is populated but `workflow.event_trigger` is not `true`, so the configured event will not launch the workflow |
| `E177` | `event_parameters[*].params[*].value` uses the form `<BaseObject.Field>` where `BaseObject` does not match the event's declared `baseObject` (from `$event_base_objects.events`) and is not prefixed by `Event.` / `DataSource.` |
| `W179` | `event_parameters[*].params[*].value` is a `<...>` token that is neither a known special token (`$event_special_tokens.tokens`) nor a statically-verified `<BaseObject.Field>`; prompts the composer to fetch `GET /notifications/email-templates/info/selections?category=<category>` via MCP before emission |
## Related files
- `workflow-enums.json` -> `standard_events`, `event_parameters_schema`
- `zuora-standard-fields.json` -> `$event_base_objects.events`, `$event_special_tokens.tokens`
- `workflow-triggers-and-linkages.md` -> "Workflow-level field derivation by trigger style"
- `workflow-skeleton.json` -> default `parameters` shape with the always-present keys
- `scripts/lint-workflow-json.js` -> rules `E007`, `E120`, `E122`, `W121`, `W123`, `E177`, `W179`
SHA-256: 21cde91869c0b3707e20ab3e15e1057492ad895b05b0081ef13032df2b0e537a