Triggers: run on save
A trigger function runs automatically whenever records on an object are inserted, updated or deleted — no button, no schedule, no person clicking anything. The save itself is the trigger.
This is how you keep data honest without anyone thinking about it: stamp a field on insert, roll a total up to a parent on update, archive children on delete. Wire it once and it just happens, every time, forever.
The one fact that shapes every trigger
Here's the rule that catches everyone the first time. When a trigger fires, its start node does not hand you the one record that changed — it hands you the list of all affected records. Save one Lead and you get a list of one; a bulk update of fifty Leads fires the trigger once with a list of fifty.
So a trigger function always loops over that list — exactly the loop you met in Modules 04 and 05. You read the list, you iterate, and you write back after the loop:
Notice the shape: the loop walks each record and an assignment sets
values on it, but the update sits after the loop and writes the whole list
at once. That's Module 05's bulkify rule, and it is not optional in a trigger — a trigger that
ran on fifty records and put a write inside the loop would do fifty separate database calls.
A trigger always receives the list of affected records — even when only one record changed. Loop over it. Never assume a single record. Build it to handle fifty and the case of one takes care of itself.
For developers — the start parameter is a list of the object
When you declare a trigger function, the input parameter the start node passes is
the list of that object, not a single record. The parameter is marked as a
list (isList: true) and typed to the object's API name:
// the start node hands you a LIST — always
[{ name: 'records', datatype: 'object',
objectApi: '<object-api>', isList: true }]The variable name is yours to choose (records is conventional). Whatever you call
it, that exact name has to be matched when you wire the trigger — Unit 3 shows the wiring shape.
Before vs After
Triggers fire at one of two moments: before the record is saved, or after. They look almost identical on the board, but they behave very differently — and choosing the wrong one is the single most common trigger mistake.
The difference comes down to two questions: does an assignment to the triggering record persist on its own, and what does a notification do? Here is the whole story, side by side.
The save hasn't happened yet, so you're editing the record on its way in.
An assignment to the triggering record auto-persists — you do
not add an update node; the values you set ride along into the save.
A notification here prevents the save — which makes Before the
natural home for validation ("block this save and tell the user why").
The record is already written. An assignment to the triggering record does
not persist — to make a change stick you must add an update
node. A notification here only shows a message; it cannot stop
the save (it already happened). After is where you do side-effects: roll up a total, create a
follow-up task, fire an email.
One thing is the same in both: writing to other objects is always allowed. Updating a parent record, creating a child, deleting a related row — fine in Before, fine in After. The Before-vs-After rules above are specifically about the record that triggered the function.
The recursion trap in After triggers
If an After trigger on an object updates that same object, that update can fire the trigger again, whose update fires it again… a loop that never settles. The fix is a condition: before you write, check whether the work is already done, and skip the update if it is.
An After trigger that updates its own object can re-fire itself endlessly. Always gate the write with a condition — "is the field already set to what I'd set it to?" — so the second pass finds nothing to do and stops. Before triggers don't have this problem: they change the record in flight, with no second save.
Wiring the trigger
Building the function is only half the job. A trigger function does nothing until it is connected to record events through workflow wiring — and the wiring has a few exact rules that, if you miss them, fail silently.
The wiring tells the platform three things: which function to run, what to pass it, and which events to listen for. It looks like this:
{
function: 'flag_overdue__fx', // note the __fx suffix
functionParams: [
{ name: 'records', // MUST match the function's input param
value: 'list-new',
type: 'list', // MUST be 'list', never 'object'
useContext: true }
],
triggers: [
{ label: 'On Create', name: 'onAfterInsert', enabled: true },
{ label: 'On Update', name: 'onAfterUpdate', enabled: true }
]
}Read it top to bottom. The function is the one you built (with its __fx
suffix). functionParams is the bridge that hands the event's records into your input
parameter. And triggers is the list of events to fire on — each with a human label,
an event name, and an enabled flag so you can turn individual events on
and off.
① functionParams type must be 'list' — not
'object'.
② The param name must match the function's input parameter exactly
(here, records).
③ The function name must include the __fx suffix.
④ Compilation is activation — there is no separate "activate" step. Compile the
workflow and it is live.
For developers — why these four, and how they fail
Each rule maps to a real failure mode:
- type
'object'instead of'list'— the event hands over a collection; declaring it as a single object means the function receives a malformed value and your loop has nothing to iterate. - name mismatch —
functionParams[].nameis bound by name to the function's declared input. If the function expectsrecordsand the wiring passesrecordList, the input arrives empty; the function runs and quietly does nothing. - missing
__fx— the suffix is how the platform resolves the compiled trigger entry point. Without it the lookup misses. - expecting an activate step — there isn't one. The compile is the activation; if it compiled, it's running.
value: 'list-new' with useContext: true tells the wiring to pull the
list of affected records from the event context and hand it in as your parameter.
Scheduled jobs
Some work isn't triggered by a save at all — it just needs to happen on a schedule. Nightly cleanups, hourly reminders, end-of-day rollups: that's a scheduled job, a function that runs on a cron timer with nobody watching.
Because no user and no record kicked it off, the scheduled_job context is the most
self-contained of all. It takes no inputs and returns no outputs
— there's nowhere for data to come from or go to. And critically, it may not contain
page nodes: there is no user session, no screen, nobody to answer a prompt.
It's almost always a batch
A scheduled job's whole job is usually "find the records that need attention and act on all of them." That is exactly Module 05's batch shape — query the set up front, loop to decide, write back in bulk after the loop:
One query before the loop, one bulk update after it — never a write
inside. A nightly job can touch thousands of records, so bulkify isn't a nicety here; it's the
difference between finishing in seconds and timing out.
"Every night at 2 a.m., flag invoices overdue by 30 days." The job queries all
invoices whose due date is more than 30 days past and whose status isn't already Overdue,
loops to set status = "Overdue" on each, then does one bulk
update after the loop. No button, no person — the cron schedule runs it while everyone
sleeps.
Interactive functions (pages)
Sometimes a button action needs to ask the user something partway
through — "which warehouse?", "are you sure?", "enter a reason." When that happens, the function
itself pauses and shows a screen by adding a page node.
The instinct of someone coming from other tools is to build a separate custom page that collects the input and then calls a function. On this platform, don't. The page lives inside the function as a node, and the flow continues right after the user answers:
The page node collects input, a condition branches on what they chose,
and the rest of the flow finishes the job — all in one function, one continuous flow.
A button action is always a function with call context
layout_button — never a standalone custom page that calls back to a
function. If the action needs user input mid-flow, add a page node inside the
function. One function, start to finish.
Page nodes need a user session — a person on the other end of the screen. So
they belong only to interactive contexts (a button, a page form). Triggers and scheduled
jobs have no UI session and therefore cannot contain page nodes. A trigger
runs on a save; a job runs on a timer; there's no one there to answer.
For developers — the page node and the five contexts
The page node is one of the real node types — alongside assignment,
query, loop, condition, create,
update, delete, notification, functionCall,
alert, job and note. (There is, as ever, no node called
"DML" — writes are create / update / delete.)
Where a page node may appear maps straight onto the five call contexts:
layout_button— interactive: page nodes allowed.page_onload/page_formButton— interactive: allowed.trigger— runs on save, no session: not allowed.scheduled_job— runs on cron, no session: not allowed.
The whole trail
That's it — you've reached the end of the trail. Look back at how far you've come: you can now build all four ways a function runs, the right way, with the rules that keep them fast and correct.
The journey, module by module
The four ways you can now make a function run
- A button — context
layout_button. A user clicks; the function acts on a record (Module 02, and the Zero Rule throughout). - An automation — context
trigger. A save fires it with the list of affected records; you loop and write after (Unit 1). - A scheduled job — context
scheduled_job. A cron timer runs a no-input batch; query before, bulk write after (Unit 4). - An interactive flow — a function with a
pagenode that asks the user mid-flow, then carries on (Unit 5).
Across all four, the same instincts carry you: declare your intent and call context first; read
before you write; branch with conditions; loop over lists; and keep every
query/create/update/delete out of your loops.
That toolkit builds most of a platform.
Keep the Function Reference open in another tab as your companion — every standard function and formula, searchable, for when you need the exact name or signature. And the whole trail is always one click away from all modules if you want to revisit a unit.
Button, trigger, scheduled job, interactive page-node flow — you can build them all. Pass the final checkpoint to claim your Automator stamp and complete Aquargin Way. Now go build something real.
Earn the Automator stamp
Five questions to complete the trail.