List Change Trigger Deep Dive — Reactive Automation, DSL Filtering & Workflow Orchestration
The List Change Trigger brings reactive automation to ApudFlow. Instead of running on a timer or waiting for an external webhook, your workflow fires automatically the moment a User List changes — whether the change happened through the UI, through another workflow, or through the API.
This guide covers everything you need to know to use it effectively.
How the List Change Trigger Works
The List Change Trigger is a reactive node — it sits in your workflow and waits for change events from a Redis Stream. When you click Run Test, it:
- Saves a subscription document in MongoDB (
list_trigger_subscriptions) with the workflow ID, list ID, trigger operations, optional DSL filter, andactive: true - From that moment on, any mutation to the watched list (add, update, delete) that passes the filter criteria creates a new workflow run with list data injected as context variables
Event Pipeline
List Mutation → Service Layer → Redis Stream (XADD)
→ Background Listener (XREAD BLOCK)
→ MongoDB Subscription Lookup
→ Operation + Filter Evaluation
→ Run Creation → Workflow Queue → Executor
Latency: Typically under 5 seconds between mutation and run creation. The listener uses XREAD BLOCK 5000 — it checks for new events every 5 seconds when idle, or instantly when events arrive.
Persistence
- Redis Stream
apudflow:stream:list-change-events— holds up to ~1000 events with a 3600s TTL - Last processed stream ID is persisted in Redis key
listener:list-change:last-id - Subscriptions are stored in MongoDB collection
list_trigger_subscriptions - If the listener restarts, it resumes from the last processed stream ID — no events are missed (except those published during the restart window, which is handled by the stream's TTL)
Configuration Deep Dive
List Selection (Autocomplete)
The listId parameter uses an autocomplete field that searches your User Lists by name. Start typing and matching lists appear. Pick one — only that list is watched.
Important: You can only watch lists you own. Shared lists or lists owned by other users are not visible in the autocomplete.
Trigger Operations (Multi-Select)
Choose one or more operations that fire the trigger:
| Operation | When It Fires | User Action |
|---|---|---|
add_row | A new row is inserted | UI "Add Row", add_row() worker, POST API |
update_rows | Existing rows are modified | UI inline edit, update_rows() worker, PUT API |
delete_row | A row is removed | UI "Delete Row", delete_row() worker, DELETE API |
Operation filtering is strict — if you select only update_rows, an add_row event is silently ignored. No run is created. This lets you build precise, focused workflows that only react to exactly what matters.
DSL Filter
The optional DSL filter is evaluated against the current rows of the list after the change. If the filter matches at least one row, the workflow fires. If no rows match, the event is silently ignored.
This is particularly useful when you share a list across multiple workflows. For example:
- Workflow A watches the same list with
status eq pending_approval— it fires when approval items appear - Workflow B watches with
status eq active AND price gt 100— it fires when high-value active items change - Workflow C watches with no filter — it fires on every change (full audit trail)
DSL Syntax Reference
| Operator | Also Written As | Meaning | Example |
|---|---|---|---|
eq | == | Equal (exact match) | status eq active |
neq | != | Not equal | status != archived |
gt | Greater than | price gt 100 | |
gte | >= | Greater than or equal | age >= 18 |
lt | Less than | price lt 50 | |
lte | <= | Less than or equal | volume <= 10000 |
contains | like | Substring match | name contains apple |
startswith | Prefix match | symbol startswith BTC | |
endswith | Suffix match | email endswith .com | |
in | Member of a list | status in (active, pending) | |
exists | Field exists (any truthy value) | notes exists |
Combinators: AND, OR (case-insensitive)
Grouping: Parentheses ( ) for complex logic
Examples:
# Simple condition
status eq active
# Range check
price gt 100 AND price lte 500
# OR logic
status eq urgent OR priority gte 8
# Mixed with grouping
(status eq active OR status eq pending) AND price gt 50
# Text matching
name contains trade AND notes exists
# List membership
symbol in (AAPL, MSFT, GOOGL)
How matchedRows Works
When a DSL filter is set and the trigger fires, the context variable trigger.matchedRows contains only the rows that matched the filter — not all rows. This is useful when you have a large list but only care about a subset:
trigger.listData— all rows (ifpassListDatais on)trigger.matchedRows— only rows that satisfied the DSL filter
passListData Toggle
When enabled (default: on), the trigger passes the full list rows to the triggered workflow as trigger.listData. When disabled, only metadata (listId, listName, operation) is passed — you would need to use a User Lists (Read) worker inside the triggered workflow to fetch the data.
When to disable: If your list has thousands of rows and passing them all as context variables would bloat the run document. In that case, read the list inside the workflow instead.
Context Variables Reference
Available in all downstream nodes when the trigger fires:
{
"trigger": {
"listId": "6a3813519ddc3a467466c561",
"listName": "Trading Journal",
"operation": "add_row",
"listData": [
{"_id": 1, "symbol": "AAPL", "price": 190, "status": "active"},
{"_id": 2, "symbol": "MSFT", "price": 420, "status": "active"}
],
"matchedRows": [
{"_id": 1, "symbol": "AAPL", "price": 190, "status": "active"}
]
}
}
Use these in downstream workers with template syntax like {{trigger.listName}}, {{trigger.operation}}, or iterate over {{trigger.listData}} in a Parameter Loop or other processing worker.
Workflow Orchestration Patterns
Pattern 1: Reactive Dashboard Update
When a journal entry is updated, recalculate and refresh all related dashboard widgets.
[List Change Trigger: update_rows]
→ [Aggregate Metrics (User Lists Read)]
→ [Update Dashboard Widgets]
→ [Log Update Timestamp]
Pattern 2: Conditional Alerting with DSL
Only notify you when high-priority items change. The DSL filter does the heavy lifting — no separate condition node needed.
[List Change Trigger: all ops, filter: "priority gte 8 OR status eq urgent"]
→ [Format Alert Message]
→ [Send Telegram / Email]
Pattern 3: Multi-Stage Data Pipeline with Chained Lists
A data collection workflow writes to a "Raw" list. The trigger fires on new rows, an AI worker enriches them, and results go into a "Processed" list.
[Raw Data List Change: add_row]
→ [AI Enrichment (classify, summarize)]
→ [Add Row to Processed List]
→ [Processed List Change Trigger → Next Pipeline Stage]
Pattern 4: Parallel Processing with Multiple Triggers
One list, three triggers — each handling a different concern:
[Shared Team List]
├── Trigger A: add_row only → [New Assignment Notification]
├── Trigger B: update_rows, filter: "status eq completed" → [Completion Handler]
└── Trigger C: delete_row only → [Cleanup & Archive]
Pattern 5: Parameter Loop + List Change Optimization
The Parameter Loop worker generates parameter combinations and stores them in a config list. Each new row triggers a backtest run.
[Parameter Loop Worker]
→ [Add Row to Config List]
→ [List Change Trigger: add_row]
→ [Backtest with Config]
→ [Store Results in Results List]
Each backtest runs independently and in parallel, since each add_row event creates a separate workflow run.
Pattern 6: Data Sync / Replication
When data changes in a source list, replicate it to an external system (PostgreSQL, Google Sheets, MongoDB Atlas).
[Source List Change: add_row OR update_rows]
→ [Transform Data Format]
→ [Send to External API / DB Connector]
→ [Log Sync Status]
Integration with Other Workers
User Lists Worker
The List Change Trigger and the User Lists worker are designed to work together. The User Lists worker performs CRUD operations from within a workflow — and any change it makes can trigger a List Change Trigger in a separate workflow.
This enables chained automation:
- Workflow A: fetch data → process → store in List → Workflow B (triggered by the change) → further processing
AI Workers
Combine with AI Classifier, AI Summarizer, or AI Data Analyzer to react to data changes with intelligent processing:
[List Change] → [AI Classifier: categorize new rows]
→ [Route to Different Workflows Based on Category]
Signal Generator
When a watchlist price changes, run a technical analysis and decide if there's a trading signal:
[Price Watchlist Update: update_rows]
→ [Fetch Full Market Data]
→ [Signal Generator: Technical Analysis]
→ [If Signal: Send Alert + Place Order]
Testing and Debugging
How to Verify a Trigger Works
- Save the trigger — click "Run Test" on the List Change Trigger node. You should see
status: activeand the list name confirmed - Make a change — go to Profile → Lists, find your watched list, and add or edit a row
- Check runs — navigate to the workflow's run history. Within a few seconds, a new run should appear with
trigger: list_change - Inspect context — open the run and check the
initialPayload.contextVars.triggersection to verifylistData,operation, andlistNameare correct
Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| Trigger doesn't fire | Operation mismatch | Check triggerOn includes the operation you performed |
| Trigger doesn't fire | DSL filter too strict | Try removing the filter temporarily to isolate |
| Trigger fired but no data | passListData is off | Enable it in the trigger config, or use a User Lists Read worker |
| Multiple runs for one change | Multiple triggers on same (workflow, list) pair | Only one trigger per pair is allowed; check for duplicates |
| Old trigger still firing | Subscription still active | Deactivate or delete the old trigger node |
Best Practices
- Start with a broad filter, then narrow — set
triggerOnto all operations first, verify the workflow fires, then restrict operations and add filters - Use matchedRows for efficient processing — if your DSL filter narrows down to a few rows, use
trigger.matchedRowsinstead of iterating overtrigger.listData - Avoid infinite loops — if Workflow A triggers on a list change and also writes to that same list, you get a feedback loop. Either use different lists for trigger and output, or add a guard variable
- One purpose per trigger — each workflow should do one thing well. Create separate workflows for notification, analysis, archiving, etc., each watching the same list with different filters
- Monitor listener health — the List Change Trigger depends on the background listener process. If the listener is down, changes are still published to Redis but not processed until the listener restarts
Legal Disclaimer
This is not financial advice. Trading involves significant risk of loss. Use reactive automation workflows at your own discretion.