Content-Aware || (OR) Operator Fallback for Collections

Doc Auto Team
Doc Auto Team
  • Updated

Overview

The || (OR) operator now automatically skips empty collections and falls back to the next available source. Collections evaluate as TRUE if they contain items and FALSE if they are empty - eliminating the need for manual .length or .count gatekeeper logic.

This update makes address fallback logic — and any multi-source collection lookup — dramatically simpler to write and maintain.

The Problem: OR Operator "Sinking"

When chaining collections with ||, the engine previously "sank" into the first collection it found — even if that collection was an empty array. It would stop there instead of evaluating the next collection in the chain.

This caused two major issues:

  • Logical Sinking — the engine stopped at an empty collection instead of falling back to the next one.
  • Code Bloat — coders had to write manual .length > 0 gatekeeper conditions for every possible combination of data presence, then repeat the entire address block for each scenario.

What Used to Fail (Now Works)

The following syntax attempts were the natural, readable way to chain collection sources for a mailing address lookup. All three failed before this update because the engine would sink into the first collection even when it was empty.  ✗  PREVIOUSLY FAILED — NOW WORKS  

{#((RES_entities || AR_entities || entities) | intersect:tasks:'entityID'
  | grab:1:'addresses' | filter:'addressStatus':'confirmed' | locationType:'Mailing')
|| ((RES_affiliations) || (affiliations | status:'!inactive') | intersect:tasks:'entityID'
  | except:(registrations | intersect:tasks:'entityID' | flatten:'affiliations'):'affiliationID'
  | filter:'role':'Shared Address'| filter:'type':'Shared Address'| flatten:'addresses'
  | filter:'addressStatus':'!inactive':'!outgoing' | locationType:'Mailing')}
{#attention}{attention}, {/}
{#street}{street}{/}
{#city}, {city}{/}
{#provinceState}, {provinceStateISO || provinceState}{/}
{#country!='Canada'} {country | upper}{/}
{/}

The engine now correctly skips an empty first-level collection (e.g. RES_entities) and falls back to the next source (e.g. RES_affiliations) automatically.

The Old Workaround: Bloated Gatekeeper Logic

To safely render a single address block from two possible sources, coders had to manually check .length conditions for every combination — and then repeat the entire rendering block for each scenario. 

  ✗  OLD APPROACH — No Longer Needed  

// Condition just to check WHICH collection has data:
{#(((((RES_entities || AR_entities || entities) | intersect:tasks:'entityID'
  | grab:1:'addresses' | filter:'addressStatus':'confirmed'
  | locationType:'Mailing').length)>0)
  &&
  ((((RES_affiliations) || (affiliations | status:'!inactive')
  | intersect:tasks:'entityID'
  | except:(registrations | intersect:tasks:'entityID' | flatten:'affiliations'):'affiliationID'
  | filter:'role':'Shared Address' | filter:'type':'Shared Address'
  | flatten:'addresses' | filter:'addressStatus':'!inactive':'!outgoing'
  | locationType:'Mailing').length)<1)}

  // Then render address block for entity source:
  {#((RES_entities || AR_entities || entities) | intersect:tasks:'entityID'
    | grab:1:'addresses' | filter:'addressStatus':'confirmed' | locationType:'Mailing' | grab:1)}
  {#attention}{attention}, {/}
  {#street}{street}{/} {#city}, {city}{/}
  {/}
{/}

// Then repeat the ENTIRE block again for affiliation source:
{#((((RES_entities || ...).length)<1) && (((RES_affiliations || ...).length)>0))}
  {#((RES_affiliations) || (affiliations | status:'!inactive') | ... | grab:1)}
  {#attention}{attention}, {/}
  {#street}{street}{/} {#city}, {city}{/}
  {/}
{/}

// ...and repeated AGAIN for every single field (attention, street, suite, city...)

This pattern had to be duplicated for every individual field: {attention}, {street}, {suite}, {city}, {provinceState}, {country} — resulting in 100+ lines of code for a single address block.

How It Works Now

Content-Aware Truthiness

Collections now automatically evaluate as a boolean:

  • Empty collection → FALSE
  • Collection with one or more items → TRUE

No .count > 0 or .length > 0 check needed inside your conditions.

Functional Fallback (Short-Circuiting)

The || operator now tries each operand left to right. If the first is empty or fails its filters, the engine moves to the next — and so on — until it finds a non-empty result.

Syntax Pattern

General Form

{#(Collection1 | filters...) || (Collection2 | filters...) || (Collection3 | filters...)}
  {field1} {field2} {field3}
{/}

Real-World Address Fallback

Try entity mailing address first; fall back to shared affiliation address if entity source is empty:

{#((RES_entities || AR_entities || entities)
  | intersect:tasks:'entityID'
  | grab:1:'addresses'
  | filter:'addressStatus':'confirmed'
  | locationType:'Mailing')
|| ((RES_affiliations) || (affiliations | status:'!inactive')
  | intersect:tasks:'entityID'
  | except:(registrations | intersect:tasks:'entityID' | flatten:'affiliations'):'affiliationID'
  | filter:'role':'Shared Address'
  | filter:'type':'Shared Address'
  | flatten:'addresses'
  | filter:'addressStatus':'!inactive':'!outgoing'
  | locationType:'Mailing')}
{#attention}{attention}, {/}
{#street}{street}{/}
{#city}, {city}{/}
{#provinceState}, {provinceStateISO || provinceState}{/}
{#country!='Canada'} {country | upper}{/}
{/}

Tips & Best Practices

💡  Chain as many collections as needed with || — the engine tries each in order until it finds a non-empty result.

💡  Filters and functions (| filter, | grab, | except, | intersect, etc.) are fully evaluated per operand before the fallback check.

💡  No need for .count > 0 or .length > 0 inside your {#if} conditions when using || — the engine handles it automatically.

💡  Works in both Word and PDF templates with no configuration required.

💡  Test your chains with intentionally empty collections to confirm fallback behavior before connecting to live templates.

💡  To limit results to the first matching item from whichever collection wins the fallback, append | limit:1 to each operand: {#(Collection1 | filters | limit:1) || (Collection2 | filters | limit:1)}...{/}.