Who this is for. Site builders and integrations engineers adding behaviour to a site without platform code. After this page you can read any job in the admin, know what each part does, and write the templates its actions use.
Jobs are under Customization › Orchestration in the admin: List for the jobs, Batch Logs for their runs, Configuration for the module settings. Jobs were called hooks before the module was renamed, and the old word survives where you will meet it: the API routes /cdna-api/hooks/… and /cdna-api/webhook/…, the class names (Hooks\Entities\Hook), and the folder templates/hooks/ in a theme repository.
The parts of a job
| Part | What it is | Where it is set |
|---|---|---|
| Triggers | What starts the job: a record event, an HTTP call to a web event, a schedule, or another job's Trigger job action. A job can have several | Entity Event Triggers, Web Event Triggers, Scheduled executions and Recurring execution on the job |
| Actions | What it does, in order: find, create, update and delete records; send an HTTP request or answer one; send an email; render a template; create or merge files; trigger another job; stop; ask an AI agent | The job's action list |
| Conditions and loops | Actions that hold other actions: a condition runs one branch, a loop runs its children once per element | In the action list, with the actions they contain |
| Job run | One execution: a status, and the status, output and data of every action that ran. The code calls it a batch | Batch Logs |
One complete job
A job read top to bottom as the admin lists it: Form: contact → notify sales, category Forms, run on one site, asynchronous.
- Entity event trigger: entity Form Submissions, method Completed, parent form set to the contact form, so no other form's submissions start it.
- Condition
is-sales, condition type Template: matches when the submission'stopicanswer issales. - Inside its branch, Send Email
notify-sales: to the sales inbox, with the submission in the body. - Inside its branch, HTTP Request
push-to-crm: aPOSTof the submission to the CRM, with the CRM's key read from site data.
The words in code type are each action's Identifier, the name later actions and the run log use for it.
The settings on the job
| Setting (admin label) | What it does |
|---|---|
| Name, Description, Category | How the job is found and understood later. See Naming and categories |
| Run on site | Limits the job to events on one site. Empty means every site of the client, each record's event running in that record's site. A job with a schedule must have a site |
| Synchronous execution | Runs the job inside the request that started it, which matters only for a web event or a Trigger job action. Jobs started by entity events always run in the background. Leave it off unless a caller needs the answer: see The synchronous flag |
| Batch persistence | Which runs are kept: Always (the default), On Error, On Success or Never. A run that is not kept is deleted when it ends, with its folder of files |
| Published | A new job is unpublished, and an unpublished job does not run on its own triggers. Publish it once its actions are in place |
Actions run with the permissions of the backend user who created the job, or of a virtual administrator when the job has no creator. A job created by an editor who cannot delete pages cannot delete pages either.
Templates in a job
Most action fields are template code, rendered with the site's engine: Smarty on a Smarty site, Twig on a Twig site. The longer bodies (Render Template, Send Email, the HTTP request body, Create File) can instead name a template file under templates/hooks/ in the theme repository. Use a file for anything longer than a line: it is versioned and reviewed with the rest of the theme. Every job template receives these variables:
| Variable | Holds |
|---|---|
event | What started the run: the record event, the web request, the scheduler task, or the job that triggered this one. Each kind is on Entity events and the pages after it |
runData | The results of earlier actions in this run: runData.get('identifier') |
customData | Values you set with set(key, value) for later actions in the same run to get |
siteData | The site's key and value store, which persists between runs: get(key) and set(key, value). A value stored for the site wins over one stored for the whole client |
query | The query helper, as in page templates: findBy, findOneBy, findCustomEntityRecordsBy |
hook, batch, site | The job, the run, and the site it runs in. site.getPrimaryHostname() is the environment's primary hostname, for absolute links |
loopRecord, loopElement | Inside a loop only: the current element. See Loops and failing iterations |
Two helpers appear only where they are needed: result in a template condition and loopHelper in a loop's template source. The page globals are not here, because a job has no visitor and no request: write query, not _query. Records reach a job as the API's models, so they offer the API's getters: read a layout area through the active revision, getActiveRevision().getAreaText('intro'), not with getAreaText() on the record. Templates in a job also see unpublished records.
Sending JSON
Request bodies, record data and trigger data are JSON, and must still be valid JSON after the template has run, whatever the content holds. A title with a quote in it breaks JSON typed by hand. Build the payload as a value and encode it: