Triggers - Best Practices

Effective Trigger Implementation

personClever Llamas
CleverLlamasMinimum Llamaverse Version: 2
databaseMinimum MarkLogic Version: 11

Triggers look simple in demos and difficult in production for one reason: they run on your write path unless you design them very deliberately.

The first design decision is always timing:

  • Pre-commit trigger: same transaction, can roll back the originating write.
  • Post-commit trigger: separate transaction, cannot roll back the originating write.

The examples in this article assume the llamaverse (v2.0+) is deployed. The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse — see the llamaverse article for full setup instructions.

Timing Model

ConcernPre-commitPost-commit
Transaction boundarySame as source writeSeparate transaction
Failure effectCan fail and roll back source writeSource write remains committed
Best fitValidation, invariants, mandatory side effectsAudit fan-out, notifications, asynchronous enrichment
Write-path latency impactDirectIndirect

A good trigger design starts by deciding whether failure must block the original write.

Pre-commit Validation Trigger

Use pre-commit for strict invariants that must hold at commit time.

xquery version "1.0-ml";
import module namespace trgr = "http://marklogic.com/xdmp/triggers"
  at "/MarkLogic/triggers.xqy";

(: Pre-commit trigger for incoming llama validation. :)

trgr:create-trigger(
  "validate-incoming-llamas",
  "Reject invalid incoming llama profiles",
  trgr:trigger-data-event(
    trgr:directory-scope("/cleverllamas/llamaverse/work/incoming/", "infinity"),
    trgr:document-content("create"),
    trgr:pre-commit()
  ),
  trgr:trigger-module(
    xdmp:database("Modules"),
    "/triggers/",
    "validate-llama-create.xqy"
  ),
  fn:true(),
  xdmp:default-permissions(),
  fn:false(),
  "normal"
)
xquery version "1.0-ml";

(: Trigger action template for pre-commit validation. :)

declare namespace trgr = "http://marklogic.com/xdmp/triggers";
declare variable $trgr:uri as xs:string external;

declare function local:assert-required-fields($doc as node()) {
  if (empty($doc/name) or empty($doc/breed))
  then fn:error(xs:QName("MISSING-REQUIRED-FIELD"), "name and breed are required")
  else ()
};

let $doc := fn:doc($trgr:uri)
return local:assert-required-fields($doc)
Runtime note (local validation):
- Re-running trigger creation can return TRGR-TNEXISTS when the trigger already exists.
- This is expected during repeatable article validation.
- For clean reruns, drop the trigger first or use idempotent setup wrappers.

If this action throws, the source write fails, which is exactly what you want for validation rules.

Post-commit Side-Effect Trigger

Use post-commit for side effects where eventual completion is acceptable.

xquery version "1.0-ml";
import module namespace trgr = "http://marklogic.com/xdmp/triggers"
  at "/MarkLogic/triggers.xqy";

(: Post-commit trigger for asynchronous audit fan-out. :)

trgr:create-trigger(
  "audit-llama-updates",
  "Write audit document after updates",
  trgr:trigger-data-event(
    trgr:directory-scope("/cleverllamas/llamaverse/raw/wild-llamas/llamas/", "infinity"),
    trgr:document-content("modify"),
    trgr:post-commit()
  ),
  trgr:trigger-module(
    xdmp:database("Modules"),
    "/triggers/",
    "audit-llama-update.xqy"
  ),
  fn:true(),
  xdmp:default-permissions(),
  fn:false(),
  "normal"
)
xquery version "1.0-ml";

declare namespace trgr = "http://marklogic.com/xdmp/triggers";
declare variable $trgr:uri as xs:string external;

(: Post-commit side-effect template. Failures here do not roll back source write. :)

xdmp:document-insert(
  "/cleverllamas/llamaverse/audit/" || xdmp:random() || ".json",
  object-node {
    "sourceUri": $trgr:uri,
    "event": "llama-document-updated",
    "capturedAt": fn:current-dateTime()
  },
  (xdmp:permission("cleverllamas-llamaverse-reader", "read")),
  ("audit", "trigger-generated")
)
Runtime note (local validation):
- Re-running trigger creation can return TRGR-TNEXISTS when the trigger already exists.
- This is expected during repeatable article validation.
- For clean reruns, drop the trigger first or use idempotent setup wrappers.

If this action fails, the original document update is still committed. Build retry and monitoring around that fact.

Security and Privilege Model

Normal data triggers run in the context of the user whose update caused the event. Do not assume implicit elevated rights. If elevated work is required, isolate it in carefully reviewed amped functions with minimal privilege scope.

Troubleshooting with Tracing

Tracing is one of the most useful ways to troubleshoot triggers because trigger failures often happen off the happy path and can be difficult to reproduce from the application layer alone.

When a trigger misbehaves, you usually need to answer questions such as:

  • Did the trigger fire at all?
  • Which URI or event matched?
  • Which branch of the action module ran?
  • Did the failure happen before or after a write or security assertion?

Named trace events are well suited to that job. MarkLogic's xdmp:trace() signals a trace event, and the event is logged when tracing for that event is enabled. xdmp:trace-enabled() lets you guard more detailed debug output so you only pay for it when you are actively diagnosing a problem. That matters with MarkLogic's execution model because trigger code runs on real transaction and write paths, not in a harmless debug sandbox, so unnecessary debug work can affect latency, log volume, and operational clarity.

For production-safe trigger support, prefer a small set of stable trace event names in trigger action modules. That gives operations a way to turn on targeted diagnostics for one trigger path without flooding the logs with unrelated noise.

Tracing is especially valuable for post-commit triggers because the source write may succeed even when the follow-on action fails. In that case, trace output is often the fastest way to reconstruct what happened inside the separate trigger transaction.

Avoiding Trigger Recursion

A common operational bug is self-firing trigger chains where the action writes content in a scope watched by the same trigger. Prevent this by:

  1. Writing action output to separate URI spaces or collections.
  2. Adding explicit guards in the action module.
  3. Using narrow trigger scopes.

Performance Guidance

  1. Keep trigger action modules small and deterministic.
  2. Avoid expensive network calls in pre-commit actions.
  3. Prefer post-commit for heavier enrichment.
  4. Keep high-volume event scopes narrow.
  5. Measure write latency before and after trigger activation.

Practical Checklist

1. Choose pre-commit only for true invariants.
2. Keep trigger action modules small and deterministic.
3. Avoid heavy network calls in pre-commit actions.
4. Guard against trigger recursion explicitly.
5. Log and monitor trigger failures with clear URIs.
6. Test with real security roles, not only admin users.

Also verify that each trigger action module has a minimal tracing strategy:

  1. One stable trace event name per trigger family or action path.
  2. Entry/exit trace points around the action's important decision boundaries.
  3. Enough URI or event-context detail to diagnose failures without dumping excessive document content.

Common Mistakes

MistakeConsequenceBetter approach
Using pre-commit for optional side effectsSlower writes and unnecessary rollbacksUse post-commit for optional work
Running heavy logic in trigger actionsThroughput degradationKeep trigger actions thin
Ignoring security contextPermission errors in productionTest with real application roles
Not handling recursionCascading writes and instabilitySeparate scopes and guard logic
Missing trigger tracing and failure observabilitySilent data-quality drift and slow incident diagnosisAdd targeted trace events, alerting, and operational dashboards

Need Some Help?


Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!
  • Post-commit Side-Effect Trigger