Transaction Codes
Reference for the tran code resource within a Twisp Ledger.
The Basics
Transaction codes (AKA "tran codes") define how ledger entries are written in Twisp. You can think of them as macros or templates for transactions; instead of writing out every transaction by hand within your application layer, you design the tran codes to match every type of transaction in your funds flow.
In a nutshell, transaction codes accept input values and write a multi-entry transaction to the ledger.
In this way, tran codes form a self-documenting API for your funds flow. The benefits of this approach are legion:
- Strong separation of concerns between accounting logic (i.e. how ledger entries need to be written to which accounts) and product/business logic.
- All transactions are executed atomically. Enforcing all-or-nothing commits leads to correct and easy-to-reason-about systems.
- Centralized interface for defining every type of transactional activity that your system handles, providing a rich view into your funds flow.
- A linear—not exponential—growth curve in system complexity as more and more transaction types are added to reflect the growth of your financial product.
With tran codes as the mechanism for structuring your transactions, you can reign in your funds flow and prevent it from becoming a tangled and unmanageable mess.
Components of Tran Codes
There are 4 primary components which define a tran code:
- Code: a unique name to identify the tran code, usually formatted in UPPER_SNAKE_CASE. When posting a transaction, this is the value supplied to the
tranCodefield to specify which tran code should be invoked. - Params: the definition of the parameters available when invoking a tran code. This allows values to be supplied at runtime which can be injected into the Transaction and Entries written by the tran code.
- Transaction: the specification of values to be used for the Transaction written. Invoking a tran code writes a single transaction into the ledger, and the values used for that transaction are defined either as literals or else are derived from values passed in
params. - Entries: the specification of values to be used for the ledger Entries written for the transaction when a tran code is invoked. The values used for these entries are defined either as literals or else are derived from values passed in
params. An entry can have acondition, so that it is written only for some postings.
Two optional components help validate and compute values:
- Assertions: named boolean expressions that must all be true for a posting to succeed.
- Vars: intermediate values computed from
params, available to the transaction and entry expressions asvars.<name>.
In addition, tran codes have other properties which are common to all records in the accounting core:
- ID: a universally unique identifier (UUID) for the tran code.
- Description: a free-form text to be used for describing anything about the tran code. We recommend using this field to summarize what the tran code is for and when it should be used.
- Created & Updated Timestamps: self-evident: when the tran code was created and when it was last updated.
- Version & History: tran codes, like every other record in the accounting core, maintain a list of all changes made to them in their
historyfield, and theversionfield indicates the current active version of the tran code.
Tran Code Invocation
Whenever you post a transaction in Twisp, you must supply a tran code to invoke. There is no way to post transactions outside of a tran code invocation. In this sense, posting a transaction is how you invoke a tran code. One cannot happen without the other.
Posting a transaction requires only three components: a transactionId to ensure idempotency for the transaction, the tranCode identifier matching the code field of the tran code to invoke, and the params object to provide parameter values for the tran code.
Example
mutation PostACHCredit(
$userAcctId: UUID!
$amount: String!
$effective: String!
) {
postTransaction(
input: {
transactionId: "66fcf002-f815-4608-ad38-8455a46d7f02"
tranCode: "ACH_CREDIT"
params: { account: $userAcctId, amount: $amount, effective: $effective }
}
) {
transactionId
tranCode {
code
}
effective
entries(first: 2) {
nodes {
amount {
money: formatted(as: { locale: "en-US" })
}
direction
layer
account {
name
}
}
}
}
}
The above example will write a Transaction and Entries as defined in the ACH_CREDIT tran code, using the parameters supplied.
Expressions and Literal Values
Every field in transaction and entries, and every entry condition, assertion and var, is a CEL expression evaluated when a transaction is posted. A text value inside an expression needs its own quotes: currency: "'USD'", entryType: "'TRANSFER_DR'". direction and layer take bare names: direction: "DEBIT", layer: "SETTLED".
A param's default is different. It is a literal value, not a CEL expression, written the way the param would be passed when posting:
| Param type | Default |
|---|---|
STRING | default: "USD" |
DECIMAL | default: "0" |
DATE | default: "2026-10-01" |
Don't quote a default the way you would quote text in an expression. default: "'USD'" is the five-character value 'USD', quotes included, and entries using it are posted in a currency named 'USD'. A DATE default must be a date: default: "date.Today()" is rejected when the tran code is created. To default to the posting date, compute it in transaction.effective, which is an expression (see CEL Variables). When effective is omitted, it is date.Today(), the current date in UTC.
Conditional Entries and Assertions
An entry with a condition is written only when the condition evaluates to true. The entries that are written must balance, so make both sides of an optional pair conditional on the same expression.
assertions is a map of named boolean expressions. They are evaluated after params and vars are resolved and before anything is written. If any assertion is false, the posting is rejected with a BAD_REQUEST error that names the assertion, and nothing is written.
This tran code charges a fee only when fee is above zero, and rejects non-positive amounts and negative fees:
mutation CreateTransferWithFee {
createTranCode(
input: {
tranCodeId: "6c2d1f80-4b7e-4a3c-9d15-2e8f7a6b5c41"
code: "TRANSFER_WITH_FEE"
description: "Move money between accounts. Charge the sender a fee when `fee` is above zero."
params: [
{ name: "fromAccount", type: UUID }
{ name: "toAccount", type: UUID }
{ name: "feeAccount", type: UUID }
{ name: "amount", type: DECIMAL }
{ name: "fee", type: DECIMAL, default: "0" }
{ name: "currency", type: STRING, default: "USD" }
]
assertions: {
positive_amount: "params.amount > decimal('0')"
fee_not_negative: "params.fee >= decimal('0')"
}
transaction: {
effective: "date.Today()"
description: "'Transfer ' + string(params.amount)"
}
entries: [
{
entryType: "'TRANSFER_DR'"
accountId: "params.fromAccount"
direction: "DEBIT"
layer: "SETTLED"
units: "params.amount"
currency: "params.currency"
}
{
entryType: "'TRANSFER_CR'"
accountId: "params.toAccount"
direction: "CREDIT"
layer: "SETTLED"
units: "params.amount"
currency: "params.currency"
}
{
entryType: "'FEE_DR'"
accountId: "params.fromAccount"
direction: "DEBIT"
layer: "SETTLED"
units: "params.fee"
currency: "params.currency"
condition: "params.fee > decimal('0')"
}
{
entryType: "'FEE_CR'"
accountId: "params.feeAccount"
direction: "CREDIT"
layer: "SETTLED"
units: "params.fee"
currency: "params.currency"
condition: "params.fee > decimal('0')"
}
]
}
) {
code
}
}
Posting it gives these results:
params (besides the three accounts) | Result |
|---|---|
amount: "100.00" | Two entries: TRANSFER_DR and TRANSFER_CR, 100.00 USD each. |
amount: "100.00", fee: "2.50" | Four entries: the transfer pair, plus FEE_DR and FEE_CR for 2.50 USD. |
amount: "0", fee: "2.50" | Rejected: assertion 'positive_amount' failed: `params.amount > decimal('0')` |
amount: "100.00", fee: "-1" | Rejected: assertion 'fee_not_negative' failed: `params.fee >= decimal('0')` |
Compare a decimal param with a decimal: params.fee > decimal('0'). The tran code is rejected at creation if a condition compares a decimal with a number, as in params.fee > 0.
Tran Code Operations
Use GraphQL queries and mutations to read, create, update, and delete (lock) tran codes:
Query.tranCode(): Get a single tran code.Query.tranCodes(): Query tran codes using index filters.Mutation.createTranCode(): Create a new transaction code.Mutation.updateTranCode(): Update select fields for an existing transaction code.Mutation.deleteTranCode(): Soft delete (lock) a specified transaction code.
Further Reading
To learn how to work with tran codes, see the relevant tutorials on Building Tran Codes and Designing a Tran Code.
For more context on how and why Twisp uses tran codes, see Encoded Transactions.
To review the GraphQL docs for the TranCode type, see TranCode.