How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Stop a job from triggering itself

Run it asynchronously where you can, guard it with a condition on Pre-Event data, write the watched field only when the write changes something, and cap chains of jobs with a depth.

Context

Context. A job on UpdateRecord that writes to the record it watches fires UpdateRecord, which runs the job again. Whether that loop can happen depends on how the job runs: records written by an asynchronous job fire no entity events, so it cannot re-trigger itself that way; a synchronous job's writes do fire them. Guard every job that writes what it listens to anyway, because a job later switched to synchronous inherits the loop, and so does one that writes through the site's API with an HTTP request. The full explanation is guarding against recursion.

Steps

  1. Run it asynchronously unless it answers a web request. That alone stops a job re-triggering itself through its own record writes.
  2. Capture the field you react to as the trigger's Pre-Event property, such as {published} or {financialStatus}.
  3. Put a condition first that carries on only when that field changed in the save that fired the job. Everything else goes inside it.
  4. Never write that field from the same job. If the job must write it (normalising a value, say), write only when the write would change something.
  5. Test it: save a record once, and count this job's batches in Batch Logs. One batch is right; two is a loop that the guard caught; a stream of them is a loop it did not catch. Unpublish the job at once to stop one.

Code

The guard, for a job that should act once when a page is first published, with {published} as the Pre-Event property:

Template code
{$before = $event->getPreEventData()}
{if empty($before.published) && $event->getRecord()->getPublished()}{$result->setMatching(true)}{/if}
{% set before = event.getPreEventData() %}
{% if before.published is empty and event.getRecord().getPublished() %}{% do result.setMatching(true) %}{% endif %}

A job that has to write the field it watches, such as one that trims stray spaces from a product's name, cannot use that guard: its own write changes the field. Make the write conditional on it changing something instead. The job's own save fires it once more, finds nothing left to do, and stops there:

Template code
{$name = $event->getRecord()->getName()}
{$tidy = trim($name)}
{if $tidy != $name}{$result->setMatching(true)}{/if}
{% set name = event.getRecord().getName() %}
{% set tidy = name|trim %}
{% if tidy != name %}{% do result.setMatching(true) %}{% endif %}

Two jobs that trigger each other with Trigger job need a different guard: pass a depth in the event data and stop when it reaches a limit.

Template code
{$in = $event->getData()}
{if ($in.depth|default:0) < 3}{$result->setMatching(true)}{/if}
{% if (event.getData().depth|default(0)) < 3 %}{% do result.setMatching(true) %}{% endif %}

Why this and not that

Compare with Pre-Event data, not with the record's current value alone. The record is read when the batch runs, so "the status is shipped" is true on every later save too; only "it was not shipped before this save" identifies the change. A guard even on an asynchronous job, because the rule that background writes fire no events is how the platform behaves today, not a promise, and it does not cover writes made through the API. And a write that is conditional, not a marker compared with the modification time: the job's own write moves the modification time, so any marker it sets is older than the change it caused, and the job runs again. A test of whether there is anything left to do cannot be fooled that way.

Go deeper

Was this page helpful?

Your answer goes to the docs team.