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
strtype, number fields arefloattype, date fields aredatetime.dateinstances. -
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_valuesproperty (e.g.field.item_amount.all_values). Its value is a special sequence objectTableColumnthat behaves similarly to alist, but with operators applying elementwise or distributive to scalars (NumPy-like). Outside a single row context, the.all_valuesproperty 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_totalevaluates to a float-like value (or other types), the value also provides anattrattribute that gives access to all field schema, field object value and field object value content API object attributes (i.e. one can writefield.amount_total.attr.rir_confidence). Attributesposition,page,validation_sources,hiddenandoptionsare 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 PythonNones. Therefore, they must not be tested for usingis None. Instead, convenience helpersis_empty(field.amount_total)anddefault_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_sourceslist 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
enumtype fields, you can modify thefield.*.attr.optionslist and it will be reflected back in the app once your hook finishes. Elements of the list are objects with thelabelandvalueattribute each. You may construct new elements as dicts with thelabelandvaluekeys. -
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
.parentattribute, or the enclosing multivalue field via.parent_multivalue. This is useful when combined with thegetattrdynamic 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/{id}/{verb}, 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.