OpenAPI SpecificationJSON

Rossum Transaction Scripts

The Rossum platform can evaluate snippets of Python code that can manipulate business transactions processed by Rossum - Transaction Scripts (or TxScripts). The principal use of these TxScript snippets is to automatically fill in computed values of formula type fields. The code can be also evaluated as a serverless function based extension that is hooked to the annotation_content event.

The TxScript Python environment is based on Python 3.12 or newer, in addition including a variety of additional predefined functions and variables. The environment has been designed so that code operating on Rossum objects is very short, easy to read and write by both humans and LLMs, and many simple tasks are doable even by non-programmers (who could however e.g. build an Excel spreadsheet).

The environment is special in the following ways:

  • Predefined variables allowing easy access to Rossum objects.

  • Some environment-specific helper functions and aliases.

  • How code is evaluated specifically in formula field context to yield a computed value.

Right now, the TxScript environment is geared just towards the annotation_content event. Ultimately, we plan to provide TxScript coverage for all provided events.

The TxScript environment provides accessors to Rossum objects associated with the event that triggered the code evaluation. The event context is generally available through a txscript.TxScript object; calling the object methods and modifying the attributes (such as raising messages or modifying field values) controls the event hook response.

Basic TxScript usage in a serverless function:

from txscript import TxScript

def rossum_hook_request_handler(payload: dict) -> dict:
    t = TxScript.from_payload(payload)
    print(t)
    return t.hook_response()

In serverless functions, this object must be explicitly imported and instantiated using a .from_payload() function. The .hook_response() method yields a dict representing the prescribed event hook response (with keys such as "messages", "operations" etc.) that can be directly returned from the handler.

Meanwhile, in formula fields it is instantiated automatically and its existence is entirely transparent to the developer as the object's attributes and methods are directly available as globals of the formula fields code.

The txscript package is published on PyPI. You can install it yourself with pip install txscript and execute scripts locally.

Pythonized Rossum objects

The TxScript environment provides instances of several pertinent Rossum objects. These instances are directly available in globals namespace in formula fields, and as attributes of the TxScript instance within serverless functions.

Fields Object

A field object is provided that allows access to the fields of annotation content.

Attributes

Object attributes correspond to annotation fields, e.g. field.amount_total will evaluate to the value of the amount_total field. The attributes behave specially:

  • The field value types are pythonized. String fields are str type, number fields are float type, date fields are datetime.date instances.

  • Since number fields are of type float, they should always be rounded when tested for equality (because e.g. 0.1 + 0.2 isn't exactly 0.3 in floating-point arithmetics): round(field.amount_total, 2) == round(field.amount_total_base, 2)

Example using all_values property:

if all(not is_empty(field.item_amount_base.all_values)):
    sum(default_to(field.item_amount_tax.all_values, 0) * 0.9 + field.item_amount_base.all_values)
  • You can access all in-multivalue field ids (table columns or simple multivalues) via the .all_values property (e.g. field.item_amount.all_values). Its value is a special sequence object TableColumn that behaves similarly to a list, but with operators applying elementwise or distributive to scalars (NumPy-like). Outside a single row context, the .all_values property is the only legal way to work with these field ids. It is also a way to access a row of another multivalue from a multivalue formula.

Example iterating over multivalue rows in a formula:

for row in field.line_items:
    if not is_empty(row.item_amount) and row.item_amount < 0:
        show_warning("Negative amount", row.item_amount)

Example iterating over multivalue rows in a serverless function:

from txscript import TxScript, is_empty

def rossum_hook_request_handler(payload: dict) -> dict:
    t = TxScript.from_payload(payload)
    for row in t.field.line_items:
        if not is_empty(row.item_amount) and row.item_amount < 0:
            t.show_warning("Negative amount", row.item_amount)
    return t.hook_response()
  • You can access individual multivalue tuple rows by accessing the multivalue or tuple field ID, which provides a list of field-like objects that provide in-row tuple field members as attributes named by their field id.

  • While field.amount_total evaluates to a float-like value (or other types), the value also provides an attr attribute that gives access to all field schema, field object value and field object value content API object attributes (i.e. one can write field.amount_total.attr.rir_confidence). Attributes position, page, validation_sources, hidden and options are read-write.

  • Fields that are not set (or are in an error state due to an invalid value) evaluate to a None-like value (except strings which evaluate to ""), but because of the above they are in fact not pure Python Nones. Therefore, they must not be tested for using is None. Instead, convenience helpers is_empty(field.amount_total) and default_to(field.amount_total, 0) should be used. These helpers also behave correctly on string fields as well.

Example using is_empty and default_to helpers:

from txscript import TxScript, is_empty, default_to

def rossum_hook_request_handler(payload: dict) -> dict:
    t = TxScript.from_payload(payload)

    if not is_empty(t.field.amount_tax_base):
        # Note: This type of operation is strongly discouraged in serverless
        # functions, since the modification is non-transparent to the user and
        # it is hard to trace down which hook modified the field.
        # Always prefer making amount_total a formula field!
        t.field.amount_total = t.field.amount_tax_base + default_to(t.field.amount_tax, 0)

    # Merge po_number_external to the po_numbers multivalue
    if not is_empty(t.field.po_number_external):
        t.field.po_numbers.all_values.remove(t.field.po_number_external)
        t.automation_blocker("External PO", t.field.po_numbers)
    else:
        t.field.po_number_external.attr.hidden = True

    # Filter out non-empty line items and add a discount line item
    t.field.line_items = [row for row in t.field.line_items if not is_empty(row.item_amount)]
    if "10% discount" in t.field.terms and not is_empty(t.field.amount_total):
        t.field.line_items.append({"item_amount": -t.field.amount_total * 0.1, "item_description": "10% discount"})
        t.field.line_items[-1].item_amount.attr.validation_sources.append("connector")
        t.field.line_items[-1].item_description.attr.validation_sources.append("connector")

    t.field.po_match.attr.options = [{"label": f"PO: {po}", "value": po} for po in t.field.po_numbers.all_values]
    t.field.po_match.attr.options += t.field.default_po_enum.attr.options
    # Update the currently selected enum option if the value fell out of the list
    if (
        len(t.field.po_match.attr.options) > 0
        and t.field.po_match not in [po.value for po in t.field.po_match.attr.options]
    ):
        t.field.po_match = t.field.po_match.attr.options[0].value

    return t.hook_response()
  • You can assign values to the field attributes and modify the multivalue lists, which will be reflected back in the app once your hook finishes. (This is not permitted in the read-only context of formula fields.) You may construct values of tuple rows as dicts indexed by column schema ids.

  • You can modify the field.*.attr.validation_sources list and it will be reflected back in the app once your hook finishes. It is not recommended to perform any operation except .append("connector") (automates the field).

  • For enum type fields, you can modify the field.*.attr.options list and it will be reflected back in the app once your hook finishes. Elements of the list are objects with the label and value attribute each. You may construct new elements as dicts with the label and value keys.

  • Outside of formula fields, you may access fields dynamically by computed schema ID (for example based on configuration variables) by using standard Python's getattr(field, schema_id). Note that inside formula fields, such dynamic access is not supported as it breaks automatic dependency tracking and formula field value would not be recomputed once the referred field value changes.

  • You may also access the parent of nested fields (within multivalues and/or tuples) via their .parent attribute, or the enclosing multivalue field via .parent_multivalue. This is useful when combined with the getattr dynamic field access. For example, in the default Rossum schema naming setup, getattr(field, "item_quantity").parent_multivalue == field.line_items.

When referring to formula field values within TxScript, note that their value may be out of date in case one of their inputs was modified during the same TxScript context - their value is recomputed only once TxScript evaluation finishes. (However, at the beginning of each hook, all formula field values are guaranteed to be up to date.) Please note that any of this behavior may change in the future.

Annotation Object

An annotation object is provided, representing the pertinent annotation.

Attributes

The available attributes are: id, url, status previous_status, automated, automatically_rejected, einvoice, metadata, created_at, modified_at, exported_at, confirmed_at, assigned_at, export_failed_at, deleted_at, rejected_at, purged_at

The timestamp attributes, such as created_at, are represented as a python datetime instance.

You can format a python datetime to your format of choice using annotation.created_at.strftime("%m/%d/%Y, %H:%M:%S").

The raw_data attribute is a dict containing all attributes of the annotation API object.

The annotation also has a document attribute. The document itself has the following attributes: id, url, arrived_at, created_at, original_file_name, metadata, mime-type, see document for more details. raw_data is also provided.

This enables txscript code such as annotation.document.original_file_name.

The annotation also has an optional email attribute. The email itself has the following attributes: id, url, created_at, last_thread_email_created_at, subject, email_from (identical to from on API), to, cc, bcc, body_text_plain, body_text_html, metadata, annotation_counts, type, labels, filtered_out_document_count, see email for more details. raw_data is also provided.

This enables txscript code such as annotation.email.subject.

In a hook context, the email is available only if emails are sideloaded.

Example of rejecting an annotation:

from txscript import TxScript

def rossum_hook_request_handler(payload: dict) -> dict:
    t = TxScript.from_payload(payload)
    if round(t.field.amount_total) != round(t.field.amount_total_base + t.field.amount_tax):
        annotation.action("reject", note_content="Amounts do not match")
    if t.field.amount_total > 100000:
        annotation.action("postpone")
    return t.hook_response()

Methods

The action(verb: str, **args) method issues a POST on the annotation API object for a given verb in the form POST /v1/annotations/&#123;id&#125;/&#123;verb&#125;, passing additional arguments as specified. (Notable verbs are reject, postpone and delete.)

Note that Rossum authorization token passing must be enabled on the hook.

TxScript Functions

Several functions are provided that map 1:1 to common extension hook return values. These functions are directly available in globals namespace in formula fields, and as methods of the TxScript instance within serverless functions.

Example of raising a message in a formula field:

if field.date_issue < date(2024, 1, 1):
    show_warning("Issue date long in the past", field.date_issue)

Example of raising a message in serverless function hook:

from txscript import TxScript

def rossum_hook_request_handler(payload: dict) -> dict:
    t = TxScript.from_payload(payload)
    if t.field.date_issue < date(2024, 1, 1):
        t.show_warning("Issue date long in the past", field.date_issue)
    return t.hook_response()

The show_error(), show_warning() and show_info() functions raise a message, either document-wide or attached to a particular field. As arguments, they take the message text (content key) and optionally the field to attach the message to (converted to the id key). If no field is passed or if the field references a multivalue column, a document-level message is created.

For example, you may use show_error() for fatals like a missing required field, whereas show_info() is suitable to decorate a supplier company ID with its name as looked up in the suppliers database.

Example of a formula raising an automation blocker:

if not is_empty(field.amount_total) and field.amount_total < 0:
    automation_blocker("Total amount is negative", field.amount_total)

The automation_blocker() function analogously raises an automation blocker, creating automation blockers of type extension and therefore stopping the automation without the need to create an error message. The function signature is the same as for the methods shown above.

Helper Functions and Aliases

Whenever a helper function is available, it should be used preferentially. This is for the sake of better usability for admin users, but also because these functions are e.g. designed to seamlessly work with TableColumn instances.

All identifiers below are directly available in globals namespace in formula fields. Within serverless functions, they can be imported as from txscript import ... (or all of them obtained via from txscript import *).

Helper Functions

The is_empty(field.amount_total) boolean function returns True if the given field has no value set. Use this instead of testing for None.

The default_to(field.order_id, "INVALID") returns either the field value, or a fallback value (string INVALID in this example) in case it is not set.

Convenience Aliases

All string manipulations should be performed using substitute(...), which is an alias for re.sub.

These identifiers are automatically imported:

from datetime import date, timedelta

import re

Formula Fields

The Rossum Transaction Scripts can be evaluated in the context of a formula-type field to automatically compute its value.

In this context, the field object is read-only, i.e. side-effects on values of other fields are prohibited (though you can still attach a message or automation blocker to another field). The annotation object is not available.

This example sets the formula field value to either 0 or the output of the specified regex substitution:

if field.order_id == "INVALID":
    show_warning("Falling back to zero", field.order_id)
    "0"
else:
    substitute(r"[^0-9]", r"", field.order_id)

The Python code is evaluated just as Python's interactive mode would run it, using the last would-be printed value as the formula field value. In other words, the value of the last evaluated expression in the code is used as the new value of the field.

In case the field is within a multivalue tuple, it is evaluated for each cell of that column, i.e. within each row. Referring to other fields within the row via the field object accesses the value of the respective single row cell (just like the row object when iterating over multivalue tuple rows). Referring to fields outside the multivalue tuple via the field object still works as usual. Thus, in a definition of field.item_amount formula, field.item_quantity refers to the quantity value of the current row, while you can still also access field.amount_total header field. Further, field._index provides the row number.

Field dependencies of formula fields are determined automatically. The only caveat is that in case you iterate over line item rows within the formula field code, you must name your iterator row.