Guide 09 · Developer docs

Automation, jobs and integration

A job is a trigger, conditions and an ordered list of actions, set up under Customization › Orchestration in the admin; jobs were called hooks before the module was renamed, and the API's routes keep that name. What starts a job, what it can do, branches and loops, email, scheduled work, integrations with other systems, and every event and action.

L2–L3
Unit 01 · The job model

Anatomy of a job

A job is a set of triggers, an ordered list of actions and a few settings. One complete job read top to bottom, the settings that matter from the first day, the variables every job template receives, and how to name jobs so a site can be handed over.

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

PartWhat it isWhere it is set
TriggersWhat 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 severalEntity Event Triggers, Web Event Triggers, Scheduled executions and Recurring execution on the job
ActionsWhat 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 agentThe job's action list
Conditions and loopsActions that hold other actions: a condition runs one branch, a loop runs its children once per elementIn the action list, with the actions they contain
Job runOne execution: a status, and the status, output and data of every action that ran. The code calls it a batchBatch 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.

  1. Entity event trigger: entity Form Submissions, method Completed, parent form set to the contact form, so no other form's submissions start it.
  2. Condition is-sales, condition type Template: matches when the submission's topic answer is sales.
  3. Inside its branch, Send Email notify-sales: to the sales inbox, with the submission in the body.
  4. Inside its branch, HTTP Request push-to-crm: a POST of 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, CategoryHow the job is found and understood later. See Naming and categories
Run on siteLimits 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 executionRuns 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 persistenceWhich 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
PublishedA 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:

VariableHolds
eventWhat 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
runDataThe results of earlier actions in this run: runData.get('identifier')
customDataValues you set with set(key, value) for later actions in the same run to get
siteDataThe 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
queryThe query helper, as in page templates: findBy, findOneBy, findCustomEntityRecordsBy
hook, batch, siteThe job, the run, and the site it runs in. site.getPrimaryHostname() is the environment's primary hostname, for absolute links
loopRecord, loopElementInside 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:

Template code
{$record = $event->getRecord()}
{$payload = [
  'id'       => $event->getRecordId(),
  'method'   => $event->getMethod(),
  'name'     => $record->getName(),
  'modified' => $record->getModified()->format('c')
]}
{$payload|json_encode}
{% set record = event.getRecord() %}
{{ {
  'id': event.getRecordId(),
  'method': event.getMethod(),
  'name': record.getName(),
  'modified': record.getModified().format('c')
}|json_encode }}

In Smarty, wrap JSON typed directly into a field in {literal}…{/literal}, because a { followed by a quote otherwise starts a tag. Encoding a value, as above, avoids this in both engines.

Reading earlier results

Each action leaves its result under its identifier, and later actions read it with runData.get(). An HTTP request leaves responseCode, responseHeaders, responseData (a JSON or XML answer, parsed) and responseBody (any other answer, as text). What every action type leaves is in the job action reference.

Template code
{$crm = $runData->get('push-to-crm')}
{$crm->responseCode}
{$crm->responseData.id}
{% set crm = runData.get('push-to-crm') %}
{{ crm.responseCode }}
{{ crm.responseData.id }}

The result is a read-only collection: read it with . or ->, loop over it, and call toArray() where a plain array is needed. Asking for an identifier the job does not have stops the run with Could not find action with identifier ….

Naming and categories

Name every job for what starts it and what it does, so the list of jobs reads as a description of the site's behaviour:

HTML
Form: contact → notify sales
Order: paid → push to warehouse
Schedule: nightly → export products CSV
Web: store-hours lookup
Job: after warehouse push → email the team

Give every action an identifier that says what it produces (find-product, push-to-crm, reply-not-found): later actions refer to it, and the run log lists it. An identifier left empty is generated, and HttpRequestAction-3 says nothing in a log. Put each job in a category by domain (Forms, Commerce, Content, Integrations), and write in the description what the job touches outside the site and who to tell when it fails.

The argument for the discipline is the handover. A site with fifty unnamed jobs cannot be handed over: nobody can say which ones are live, what they touch, or whether one can be removed without breaking a partner, and the next team finds out by deleting one.

Credentials

A key a job sends to another system belongs in site data, set separately on each environment, never in a template in the repository or typed into the action. The reasons and the practice are on Credentials for a third party.

Go deeper

Was this page helpful?

Your answer goes to the docs team.