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:

  1. 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 tranCode field to specify which tran code should be invoked.
  2. 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.
  3. 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.
  4. 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 a condition, 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 as vars.<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 history field, and the version field 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 typeDefault
STRINGdefault: "USD"
DECIMALdefault: "0"
DATEdefault: "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:

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.

Previous
Money Formatting