Control Flow
Category: Control flow
Control-flow activities change the path a transaction takes through the workflow instead of processing documents. Use them to branch and route.
If Condition
Asks one question about the transaction and sends it down one of two paths.
When a transaction reaches an If step the condition is checked once, and the whole transaction leaves by one of the two exits:
- Yes: the condition held.
- No: it did not.
Only the steps on the exit taken are run; the other side is skipped. The two sides may lead back to the same step later, so a branch is not a permanent split.
Setting the condition
Pick a preset in the step's Configure panel and fill it in. The presets cover most routing decisions without any code:
| Preset | Branches on |
|---|---|
| There are issues to review | Whether anything in the transaction still needs a human: validation errors, validation warnings, low-confidence splitting, classification or fields, and unanchored items to redact. |
| Business validation rule states | An AND/OR combination of specific business rules reaching a chosen status. |
| Document count comparison | How many documents the transaction holds. |
| Page count comparison | The total page count. |
| Has document of class | Which document types the transaction holds. |
| Transaction metadata | Transaction metadata values. |
| Field value comparison | Extracted fields compared against values, picked from the schema by document type. |
| Last review decided by | Who took the last review decision: a person, a person through an assistant, or an agent. |
| Custom JavaScript | Anything else (see below). |
Most presets are a short form of switches and boxes. Four of them ask for more than one thing and open their own editor instead: There are issues to review (below), and the three that combine conditions with AND / OR (also below), alongside Business validation rule states.
Every preset generates JavaScript, which you can see (and edit) with Edit JavaScript directly. Editing it turns the step into a code condition; what that code may reach and call is under Condition runtime interface below.
There are issues to review
This preset asks the question a Human Review step asks in When is review required?, but as a branch, so a transaction can be routed on it without pausing. It is TRUE when the transaction carries at least one thing a reviewer would have to act on.
Each kind of issue is a switch, and the three confidence ones carry their own threshold:
| Setting | Counts as an issue when |
|---|---|
| Validation errors | A business rule failed at error level, at field, document or transaction level. |
| Validation warnings | A business rule failed at warning level, at the same three levels. |
| Low-confidence splitting | A document's split decision was made below the splitting threshold and has not been confirmed. |
| Low-confidence classification | A document's type was chosen below the classification threshold and has not been confirmed. |
| Low-confidence fields | An extracted value is below the field threshold, is not empty, and has not been confirmed. Counted per scalar field, per group member and per table cell. |
| Unanchored items to redact | A Redact step found a value on the page but could not place it in the recognized text, so its pixels may still be visible until a reviewer draws the region. |
Each threshold is a confidence between 0 and 1, and defaults to 0.8: a value recognized with less than 80% confidence counts as low confidence. They are the same three thresholds the Human Review step keeps under What counts as an issue, but set here, on this step, because an If usually runs before any Human Review has stamped anything.
Turning a switch off removes that kind from the question entirely. With everything off the condition is never true.
Last review decided by
Routes on who answered for the transaction's last review decision, so a workflow can treat an agent's decision differently from a person's: send a sample of what agents accepted to a person, or send one document class on to a second review. Choose one of:
| Decided by | Means the last decision was taken by |
|---|---|
| a person | Someone in the app, including through the in-app assistant. |
| a person through an assistant | Someone through their own assistant: an agent they connected, or their personal API key. The decision is still theirs. |
| an agent | An agent on its own, the built-in agent or one on an integration key, at an Agent Review step. |
The condition is FALSE for all three while no review has been decided, and a transaction whose Agent Review step failed without a decision keeps whatever was decided before it, if anything. A decision taken before reviewer kinds were recorded counts as a person's. To route a Switch three ways, give it one case per kind. In code, the same value is transaction_metadata.last_human_review.reviewer_kind (see The review record).
Steps configured with No validation errors or Classification confidence keep running and keep showing the preset they were built with. Neither was replaced in place, so nothing changed underneath an existing workflow; they are simply no longer offered for new steps.
- No validation errors asks the opposite question (TRUE when the transaction is clean). To move a step over, pick There are issues to review and swap the two branches.
- Classification confidence averaged the confidence across all documents, so one confident document could hide an unsure one. There are issues to review looks at each document on its own instead.
Combining conditions
Has document of class, Transaction metadata, Field value comparison and Business validation rule states are not limited to one question each. All four use the same AND / OR editor as a business rule's trigger, so the controls are the ones you already know:
- Rows inside one dashed box are joined by AND. All of them must hold. + AND adds a row.
- Boxes are joined by OR. Any one box holding is enough. + OR branch adds a box.
So to route a transaction that holds an Invoice and either a Credit Note or a Receipt, write the Invoice into both boxes:
has a document of
InvoiceAND has a document ofCredit NoteOR
has a document of
InvoiceAND has a document ofReceipt
The same shape works for metadata, for field values, and for business rule states.
In Field value comparison each row picks its field from the extraction schema rather than asking you to type a name:
- Document type chooses whose fields the list offers, and restricts the row to documents of that type. Leave it on Any type to compare the general (class-less) fields across every document.
- Field then lists that type's fields, with a table's columns shown as
Line Items[] / Amount. - The first dropdown reads any / every document for a plain field and any / every cell for a table column, because a column is compared cell by cell across every row.
A step saved before the picker existed keeps comparing the field name it stored, across every document, exactly as it did. A row you have not finished filling in is ignored rather than treated as false, so a condition keeps working while you edit it. A box whose rows are all unfinished is dropped, and a preset with nothing filled in at all is never true.
There is no nesting: a box cannot contain a box. Anything you would write with brackets is written by repeating the shared row in each branch, as the Invoice above is.
A step saved before this existed opens as the single condition it always was, and keeps behaving identically until you add to it.
Switch
Routes a transaction to one of several named branches.
When a transaction reaches a Switch step the cases are asked in order, from the top. The first case that holds wins and the whole transaction leaves by that branch; the cases below it are not asked at all. If no case holds, the transaction leaves by the default branch.
Only the steps on the branch taken are run; the others are skipped. As with an If, the branches may lead back to the same step later.
Use a Switch for multi-way routing, for example one branch per document class.
Setting the cases
| Setting | Description |
|---|---|
| Cases | An ordered list. Each case has a branch label, which names its exit on the canvas, and its own condition. |
| Default branch label | Names the branch taken when no case matches. |
Each case's condition is configured exactly like an If condition: pick a preset and fill it in, or choose Custom JavaScript and write it. Every preset in the table above is available on every case, including combining conditions with AND / OR. Order is a setting here, so put the most specific case first; Move up and Move down reorder them.
Each case carries its own editor, its own Describe when this case is taken box and its own reference rail, all folded away until you open them, so a long list of cases stays readable.
A case condition has the same runtime interface as an If condition, described in full below: the same globals, the same five helpers, the same reads-only sandbox, and the same way of naming a field. The one difference in meaning is that returning false falls through to the next case rather than taking a "no" branch.
Condition runtime interface
This section applies to both steps, and to the condition of a filtered Export profile. An If condition, each case of a Switch and an Export condition are the same kind of code, written in the same editor, with the same globals, the same helpers and the same rules for naming a field. The only difference is what returning false means: an If takes its No exit, a Switch case falls through to the next case, and an Export profile skips the transaction or the document.
Choosing Custom JavaScript lets you write the condition yourself. The code runs in the same isolated sandbox as business rules and Transform: plain ES2020 JavaScript, no require, no import, no filesystem, no timers, no network, and no catalog access.
A condition reads only. There is no way to change a field, a document, the transaction metadata or a catalog from one. Put a change in a Transform step, and a check that has to report to a reviewer in a business rule.
Returning a branch
Your code is a function body and must return a boolean.
return document_count > 1;
The step compares what you return with === true. Anything else takes the No branch, however truthy JavaScript itself would consider it: a number, a non-empty string, an object, an array, undefined. This is the most common mistake in a hand-written condition, and it is silent:
// WRONG: a number is not `true`, so this always takes the No branch
return documents.length;
// Right
return documents.length > 0;
In a Switch case, read "takes the No branch" as "falls through to the next case": a case that returns a number rather than true is silently not the case that wins.
A condition that throws is different, and it is not silent. It does not take a branch at all: the step fails, and the transaction stops there instead of being routed. In a Switch, a throwing case fails the whole step rather than falling through to the next one. So guard anything that might not be there:
// Throws when Validate has not run earlier in this workflow, which fails the step
return validation_results.overall_status === 'passed';
// Right
return !!validation_results && validation_results.overall_status === 'passed';
Available globals
Everything here is read-only.
| Global | What it holds |
|---|---|
documents | Array of documents in the transaction, in order. Each has document_id, document_type (the class name), page_indices, extracted_fields and metadata. |
document_count | How many documents the transaction holds. Same as documents.length. |
page_count | Total pages across every document. |
transaction | The transaction record (id, metadata, source_files, …). |
transaction_metadata | The transaction's metadata, already read for you: the pairs given at upload, and the platform's own record, including the review decisions (see The review record). |
document | The document being decided, in an Export profile filtered per document, where the condition runs once for each document. null in an If or Switch step and in a profile filtered for the whole transaction. |
document_metadata | The metadata of document in an Export profile filtered per document. Everywhere else it is {}, because the condition runs for the whole transaction: use get_document_metadata(). |
validation_results | What Validate found: overall_status, document_results, stats. null until a Validate step has run earlier in this workflow. |
workflow_context | What the steps before this one produced, keyed by activity (extract_output, classify_output, …). Steps later in the workflow are simply absent. |
Helpers
| Helper | Effect |
|---|---|
get_field_value(doc, nameOrId) | Returns a field value off one document, matched by field id or display name, or undefined if that document has no such field. Matches top-level fields only. |
get_transaction_metadata(key?) | Returns one metadata key, or the whole object if no key is given. |
get_document_metadata(docIdOrIndex, key) | Returns one key of one document's metadata. The first argument is a document_id or a position in documents. |
normalize(text, options?) | Cleans text. Options: trim, lowercase, uppercase, removeSpaces, removeSpecialChars, foldDiacritics. |
fold_text(text) | Lower-cases and removes accents for comparing two values: Şişli and SISLI both become sisli. |
strip_diacritics(text) | Removes accents but keeps the case, for a value that is stored back: Gökçe becomes Gokce. |
resolve_country(input, format?) | Maps a country name to an ISO code (alpha2 by default), or null if unresolved. |
There is nothing else. A helper that is not on this list (set_error, set_field, get_field, fetch) does not exist in a condition, and the editor flags a call to one before you save.
The review record
Every review decision is recorded in transaction_metadata: the last one as last_human_review, and every one, oldest first, in review_history. "Human" in the name means a reviewer rather than automatic processing: an agent's decision is recorded there too. Both are absent until someone decides a review.
| Key | What it holds |
|---|---|
step_id, step_name | The step where the decision was taken. |
reviewer_name | Who decided: the person's name, or for an agent the name of its integration key. |
reviewer_email | The person's email; null for an agent. |
reviewer_kind | person (in the app, including the in-app assistant), person_via_agent (through an agent the person connected, or their personal API key) or agent (an integration key). Absent on decisions taken before it was recorded, which were all a person's. |
reviewer_via | For person_via_agent, the assistant: kind (oauth for a connected agent, api_key for a personal key), client_name (the agent's or the key's name), may_teach (whether that connection allowed memory to learn from the decision, when it was taken) and, by kind, connection_id or key_suffix and description. null otherwise. |
action_type, action_name | accept, reject or send_to, and the action's name as the step shows it. |
comment | The decision's comment, or null. |
completed_at | When it was decided (ISO 8601). |
findings | What the reviewer concluded, when it said, one per issue it handled. Each has a kind (corrected, confirmed, document_defect, unresolved or observation) and its reason in reason, or in note for findings written before reason existed and by agents that still send note: read f.reason ?? f.note. It may name the issue (issue_id), a document_id (absent for a finding about the whole transaction) and a field, and list its changes, each with document_id, field, old_value, new_value and page. Absent when none were given. |
// Every decision an agent took on this transaction, not only the last one.
const history = transaction_metadata.review_history || [];
return history.some(r => r.reviewer_kind === 'agent' && r.action_type === 'accept');
How to refer to a field
get_field_value takes a field's display name, exactly as it appears in the extraction schema, or its field id. There is no class prefix: a document sees the general (class-less) fields and its own class's fields as one flat set, and where the two share a name the general one wins, so pass the field id to reach the other.
The first argument is always a document, so pick the document before reading it, and guard the pick: find() answers undefined when nothing matches, and the next line that reads a property off it throws, which fails the step. (get_field_value itself is safe on an absent document and answers undefined.)
// One particular document type
const invoice = documents.find(d => d.document_type === 'Commercial Invoice');
if (!invoice) return false;
return parse_number(get_field_value(invoice, 'Total')) > 10000;
// Any document at all
return documents.some(d => parse_number(get_field_value(d, 'Total')) > 10000);
Values arrive as extracted, which means text: read numbers with parse_number(v), which understands $12,000.00 and 12.000,00 EUR and returns null when there is no number (where Number(v) gives NaN), read dates with new Date(v), and treat '', undefined and null as "no value" rather than as zero.
Field paths do not work here. A code rule accepts group and cell paths such as 'Vendor/Name' and 'Line Items[1]/Amount'. A condition uses the same flat lookup as a Transform step, so a path matches nothing and get_field_value returns undefined. Fetch the group and traverse its value instead:
// A field inside a group: the group's value is keyed by child field id
const vendor = get_field_value(doc, 'Vendor');
const name = vendor && vendor['ch_name'] ? vendor['ch_name'].value : undefined;
// A repeating group (table): one entry per row, each keyed by column id
const lines = get_field_value(doc, 'Line Items');
return !!lines && lines.instances.some(r => parse_number(r['col_amt']) > 1000);
The value shapes are documented in full under Groups and repeating groups.
Writing a condition from a description
Under the code editor, Describe when the true branch is taken (in a Switch case, Describe when this case is taken) turns a sentence into a condition. What comes back is checked the way a save is (syntax, and calls to helpers that do not exist) and shown as a diff you accept or discard. Nothing is saved until you accept it.
The reference rail under the editor lists, for this project: what the condition can reach (every document type and every field by name), what you can call, and how to name a field, with your own field names in the examples. Clicking an entry inserts it at the cursor.