suideveloper
All questions
Transactions

What is a programmable transaction block (PTB)?

November 10, 2026

A programmable transaction block (PTB) is the format of every user transaction on Sui. It's a list of inputs plus an ordered list of up to 1,024 commands, such as Move calls, coin splits and merges, transfers and publishes. Each command can use the results of earlier ones, and the whole block is atomic: if any command fails, none of the effects apply. That lets you chain calls across different packages from the client without deploying a new contract.

Structure

Text
{
  inputs:   [Input],    // objects you own, shared/immutable objects, or pure values
  commands: [Command],  // executed in order
}

The available commands are:

Command What it does
MoveCall Call a public or entry function in any package
SplitCoins Split new coins off a coin
MergeCoins Merge coins into a destination coin
TransferObjects Send objects to an address
MakeMoveVec Build a vector<T> from arguments
Publish / Upgrade Publish or upgrade a package

Arguments point to one of four things: Input(i), Result(i), NestedResult(i, j) for one of several return values, or GasCoin, the SUI coin paying for gas.

Example: one transaction, three steps

This splits 1 SUI off the gas coin, passes it to a Move function, and sends whatever the function returns to the sender. It uses @mysten/sui v2:

TypeScript
import { SuiGrpcClient } from '@mysten/sui/grpc'
import { Transaction } from '@mysten/sui/transactions'

const client = new SuiGrpcClient({
  network: 'testnet',
  baseUrl: 'https://fullnode.testnet.sui.io:443',
})

const tx = new Transaction()
const [payment] = tx.splitCoins(tx.gas, [1_000_000_000]) // 1 SUI in MIST
const [item] = tx.moveCall({
  target: `${PACKAGE_ID}::shop::buy`,
  arguments: [tx.object(SHOP_ID), payment],
})
tx.transferObjects([item], signer.toSuiAddress())

const result = await client.signAndExecuteTransaction({ signer, transaction: tx })

The same transaction from the CLI, using sui client ptb:

Terminal
sui client ptb \
  --split-coins gas "[1000000000]" --assign payment \
  --move-call $PKG::shop::buy @$SHOP payment --assign item \
  --transfer-objects "[item]" @$ME

Execution rules worth knowing

  • Gas is reserved up front. The full gas budget comes out of the gas coin at the start, and whatever isn't used is returned at the end.
  • Values have to be consumed. An object returned by a command must be transferred, passed on to another command or destroyed. If a value without drop is left over, the whole PTB fails.
  • Move calls can't return references (&T, &mut T) to the PTB.
  • Private generics. A PTB isn't a module, so it can't call functions like transfer::transfer that require the type to be defined in the calling module. Use public_transfer and public_share_object instead.
  • What you can call. A PTB can call any public function and any entry function, including private entry functions. Other packages can't call private entry functions. Arguments to a non-public entry function can't be tied up with an unconsumed hot-potato value. See entry vs public.
  • TxContext is injected automatically. You never pass it yourself.
  • No loops or branches. A PTB is a straight-line program. If you need control flow, write a Move function and call it.

Why it matters

  • Composability without deploying. You can combine a DEX swap, a lending deposit and a transfer in one transaction that you build in the client.
  • Fewer round trips and one gas payment. Ten operations in one PTB cost less than ten separate transactions.
  • Atomicity. A multi-step flow either finishes completely or leaves no trace, apart from the gas you pay.

Limits such as the 128 KiB transaction size and 16 KiB per pure argument are listed in object and transaction size limits. To try PTBs without setting anything up, get testnet SUI from the faucet and build one in the playground.

Sources