# What is a PTB?

*[Documentation index](/llms.txt) · [Full index](/llms-full.txt)*

A programmable transaction block (PTB) is a group of commands that complete a transaction on Sui.

Transactions on Sui consist of groups of commands that execute on inputs to define the result of the transaction. Referred to as **programmable transaction blocks (PTBs)**, these groups of commands define all user transactions on Sui. PTBs allow you to call multiple Move functions, manage your objects, and manage your coins in a single transaction without publishing a new Move package. Designed with automation and transaction builders in mind, PTBs are a lightweight and flexible way of generating transactions.

However, PTBs do not support more intricate programming patterns, such as loops. In such cases, you must publish a new Move package.

The individual transaction commands within a PTB execute in order. You can use the results from one transaction command in any subsequent transaction command within the PTB. The runtime applies the effects of each transaction command in the block, such as object modifications or transfers, atomically at the end of the transaction. If one transaction command fails, the entire block fails and none of the effects from the commands apply.

A PTB can perform up to 1,024 unique operations in a single execution, whereas transactions on traditional blockchains would require 1,024 individual executions to accomplish the same result. This structure also results in cheaper gas fees. The cost of facilitating individual transactions is always more than the cost of those same transactions blocked together in a PTB, resulting in cheaper gas fees.

## What are the components of a PTB? {#transaction-components}

There are two parts of a PTB that are relevant to execution semantics. Execution might reference other transaction information, such as the transaction sender or the gas limit, but that information is out of scope. The structure of a PTB is:

```rust
{
    inputs: [Input],
    commands: [Command],
}
```

Looking closer at the two main components:

- The `inputs` value is a vector of arguments, `[Input]`. These arguments are either objects or pure values that you can use in the commands. The sender either owns the objects, or the objects are shared or immutable objects. The pure values represent basic Move values, such as `u64` or `String` values, which you can construct purely from their bytes. For historical reasons, `Input` is `CallArg` in the Rust implementation.

- The `commands` value is a vector of commands, `[Command]`. The following list shows the possible commands as TypeScript SDK builder methods (`tx.splitCoins` and so on). The execution sections later on this page refer to the same commands by their underlying names (`SplitCoins`, `MergeCoins`, `TransferObjects`, `MoveCall`, `MakeMoveVec`, `Publish`, `Upgrade`). The possible commands are:

- `tx.splitCoins(coin, amounts)`: Creates new coins with the defined amounts, split from the provided coin. Returns the coins so that you can use them in subsequent transactions.

  - Example: `tx.splitCoins(tx.gas, [tx.pure.u64(100), tx.pure.u64(200)])`

- `tx.mergeCoins(destinationCoin, sourceCoins)`: Merges the sourceCoins into the destinationCoin.

  - Example: `tx.mergeCoins(tx.object(coin1), [tx.object(coin2), tx.object(coin3)])`

- `tx.transferObjects(objects, address)`: Transfers a list of objects to the specified address.

  - Example: `tx.transferObjects([tx.object(thing1), tx.object(thing2)], tx.pure.address(myAddress))`

- `tx.moveCall({ target, arguments, typeArguments })`: Executes a Move call. Returns whatever the Sui Move call returns.

  - Example: `tx.moveCall({ target: '0x2::devnet_nft::mint', arguments: [tx.pure.string(name), tx.pure.string(description), tx.pure.string(image)] })`
- `tx.makeMoveVec({ type, elements })`: Constructs a vector of objects that you can pass into a moveCall. You need this command because there is no other way to define a vector as an input.

  - Example: `tx.makeMoveVec({ elements: [tx.object(id1), tx.object(id2)] })`

- `tx.publish(modules, dependencies)`: Publishes a Move package. Returns the upgrade capability object.

- `tx.upgrade(modules, dependencies, packageId: EXAMPLE_PACKAGE_ID, ticket)`: Upgrades an existing package. The runtime does not call `init` functions for upgraded modules.

The following table maps each TypeScript SDK builder method to its underlying command name:

| **TypeScript SDK method** | **Command name** |
| :---- | :---- |
| `tx.splitCoins` | `SplitCoins` |
| `tx.mergeCoins` | `MergeCoins` |
| `tx.transferObjects` | `TransferObjects` |
| `tx.moveCall` | `MoveCall` |
| `tx.makeMoveVec` | `MakeMoveVec` |
| `tx.publish` | `Publish` |
| `tx.upgrade` | `Upgrade` |

Learn more about each [PTB command](/references/ptb-commands).

## Inputs and results

Inputs are the values that you provide to the PTB, and results are the values that the PTB commands produce. The inputs are either objects or basic Move values, and the results are arbitrary Move values, including objects. Inputs and results form the data flow through a PTB's execution.

You can think of them as populating an array of values. For inputs, there is a single array, but for results, there is an array for each individual transaction command, creating a 2D-array of result values. You can access these values by borrowing, mutably or immutably, by copying, if the type permits, or by moving, which takes the value out of the array without re-indexing.

Learn more about [inputs and results](/develop/transactions/ptbs/inputs-and-results).

## `Argument` structure and usage

Commands take `Argument` values that specify which input or result to use. The runtime infers whether to pass the argument by reference or by value based on the command's expected type.

The `Argument` enum has 4 variants, shown here in the notation of the Rust implementation (the same notation the execution examples on this page use) rather than as TypeScript SDK calls:

- `Input(u16)`: References an input by its index. For example, with inputs `[Object1, Object2, Pure1, Object3]`, use `Input(0)` for `Object1` and `Input(2)` for `Pure1`.

- `GasCoin`: References the SUI coin used to pay for gas. When using [address balance gas payment](#what-is-address-balance-gas-payment) (empty gas payment list), `GasCoin` references a synthetic coin created from the sender or sponsor address balance. **This is separate from other inputs because it has special restrictions**:

    - `&mut` or `&` usage anywhere.
    - Only by value through `TransferObjects` or `sui::coin::send_funds`
        - If you need an owned `Coin<SUI>` from the `GasCoin`, first use `SplitCoins`.

- `NestedResult(u16, u16)`: References a result from a previous command. The first index specifies the command, the second specifies which result from that command. For example, if command 1 returns `[Value1, Value2]`, use `NestedResult(1, 0)` for `Value1` and `NestedResult(1, 1)` for `Value2`.

- `Result(u16)`: For a command at index `i`, `Result(i)` is shorthand for `NestedResult(i, 0)`, but only valid when the command at index `i` returns exactly 1 result. Use `NestedResult` for commands that return multiple results. A command that returns 0 results has no result to reference.

The following table summarizes each variant:

| **Variant** | **References** | **Usage rules** |
| :---- | :---- | :---- |
| `Input(u16)` | An input, by its index in the inputs array | Index into `[Input]` |
| `GasCoin` | The SUI coin that pays for gas | By reference (`&mut` or `&`) anywhere; by value only through `TransferObjects` or `sui::coin::send_funds` |
| `NestedResult(u16, u16)` | A result from a previous command | First index is the command, second index is the result from that command |
| `Result(u16)` | Shorthand for `NestedResult(i, 0)` | Only valid when the command at index `i` returns exactly 1 result |

## How does a PTB execute? {#execution}

When a PTB executes, the runtime populates the input vector with the input objects or pure value bytes. The runtime then executes the transaction commands in order and stores the results in the result vector. Finally, the runtime applies the effects of the transaction atomically.

### Start of execution

At the beginning of execution, the PTB runtime loads the input objects into the input array. The network already verifies the objects, checking rules like existence and valid ownership. The runtime also loads the pure value bytes into the array but does not validate them until usage.

The most important consideration at this stage is the effects on the gas coin. At the beginning of execution, the system withdraws the maximum gas budget (in terms of SUI) from the gas coin. The system returns any unused gas to the gas coin at the end of execution, even if the coin has changed owners.

### Object consumption

You must either consume all objects that Move commands create or return (destroy them, transfer them, or use them in another command) or explicitly drop them if the type has the `drop` ability.

In a PTB, if you create an object through a Move command and do not destroy, transfer, or use it in a subsequent command, the transaction fails with an error.

### What is checked before a PTB runs? {#pre-execution-validation}

When you sign a transaction, the network performs the following validations on specific commands:

- [`SplitCoins`](/references/ptb-commands) and [`MergeCoins`](/references/ptb-commands): Verifies that the argument arrays (`AmountArgs` and `ToMergeArgs`) are non-empty.

- [`MakeMoveVec`](/references/ptb-commands): Verifies that the command specifies the type for an empty vector of `Args`.

- [`Publish`](/references/ptb-commands): Verifies that the `ModuleBytes` are not empty.

### Argument usage rules {#arguments}

You can use each argument by reference or by value. The usage depends on the type of the argument and the type signature of the command:

- If the signature expects an `&mut T`, the runtime checks the argument has type `T` and then mutably borrows it.

- If the signature expects an `&T`, the runtime checks the argument has type `T` and then immutably borrows it.

- If the signature expects a `T`, the runtime verifies the argument has type `T`, then copies the value if `T: copy` or moves it otherwise. The runtime always moves objects because `sui::object::UID` does not have the `copy` ability.

The transaction fails if you use an argument in any form after the runtime moves it. There is no way to restore an argument to its position (its input or result index) after the runtime moves it.

If the runtime copies an argument that does not have the `drop` ability, it infers the last usage to be a move. As a result, if an argument has `copy` and does not have `drop`, the last usage must be by value. Otherwise, the transaction fails because a value without `drop` remains unused.

#### Borrowing rules

A borrow gives a command access to a value through a reference while the value stays in its input or result position. A mutable borrow (`&mut T`) lets the command change the value. An immutable borrow (`&T`) only lets the command read the value.

Borrowing follows additional rules to ensure safe reference usage:

- **Mutable borrow:** No other borrows can be outstanding. An overlapping mutable borrow could create dangling references.

- **Immutable borrow:** No mutable borrows can be outstanding. Multiple immutable borrows can coexist.

- **Move:** No borrows can be outstanding. Moving a borrowed value invalidates existing references.

- **Copy:** Allowed regardless of outstanding borrows.

#### Special object handling

Object inputs have the type `T` of the underlying object. `ObjectArg::Receiving` inputs are the exception and have type `sui::transfer::Receiving<T>`. This wrapper indicates that another object, not the sender, owns the object. Call `sui::transfer::receive` with the parent object to unwrap it and prove ownership.

Shared objects have restrictions on by value usage to ensure they remain shared or are deleted by the end of the transaction:

- You cannot use read-only shared objects (marked as not `mutable`) by value.

- You cannot transfer or freeze shared objects. These operations succeed during execution but cause the transaction to fail at the end.

- You can wrap shared objects or convert them to dynamic fields during execution, but you must re-share or delete them before the transaction completes.

#### Move call rules

PTBs can call any `public` function and any `entry` function, whether private (`entry fun f()`), or `public(package)` (`public(package) entry fun f()`). PTBs cannot call non-entry private and `public(package)` functions. In this way, there is no reason to add `entry` to a `public` function.

:::info

Previously, `entry` functions had signature restrictions that limited their parameter and return types compared to `public` functions. These restrictions no longer apply. `entry` functions can now have the same signature as any `public` function.

:::

**Return types:**
Move calls cannot return references (`&T` or `&mut T`).

**Private generics:**
Some framework functions have type parameters that you can only instantiate with types defined in the same module. Because PTBs are not modules, they cannot supply these types and therefore cannot call these functions directly. For example, certain `sui::transfer` functions like `transfer` and `share_object` require a type defined in the calling module, so a PTB cannot call them. Instead, use the `public_transfer` and `public_share_object` variants.

**`TxContext` handling:**
The runtime automatically injects `TxContext` parameters (`&TxContext` or `&mut TxContext`); callers do not supply them. `TxContext` can appear at any position in the parameter list, and a function can have multiple `TxContext` parameters as long as they are immutable (`&TxContext`). These parameters do not count toward the user-supplied argument count for indexing purposes.

#### Non-public entry function restrictions

A non-public `entry` function is a function declared with `entry` that is **not** `public`. It is either private (`entry fun f()`) or `public(package)` (`public(package) entry fun f()`). PTBs can call these functions directly, but other packages cannot.

Non-public `entry` functions have a single restriction: their arguments cannot be in a hot clique when the PTB calls the function. A [hot potato](https://move-book.com/programmability/hot-potato-pattern) is a value whose type has neither the `drop` nor the `store` ability, so you must consume it before the transaction completes. A clique is a group of values that commands have used together, and it is hot while it is entangled with hot potato values that the PTB has not yet consumed. This restriction ensures that arguments to non-public `entry` functions are not entangled with outstanding hot potato values that could force behavior after the function executes.

**Hot potato values**

A value is a hot potato if its type has neither the `drop` nor the `store` ability. You must consume (move by value) hot potato values before the transaction completes; you cannot silently drop or store them.

**Cliques**

The system tracks which values are entangled using cliques. Cliques exist to prevent hot potato values from forcing behavior outside of the call to the non-public `entry` function. A value becomes entangled with a hot potato when a command uses them together, which merges their cliques. The cliques provide a modeling of this entanglement by keeping a count of how many hot potato values are live and which values they have interacted with:

- Each PTB input starts in its own clique with a hot count of 0.
- When a command uses values together as arguments, their cliques merge. The hot counts add together.
- Hot potato return values from a command increment the merged clique's hot count.
- Moving (consuming) a hot potato decrements its clique's hot count.
- Before a non-public `entry` call, the merged clique of its arguments must have a hot count of 0.

A non-public `entry` function can consume hot potato values, but they must be the last hot values in their clique. The hot count check happens after argument consumption but before function verification.

**Shared objects consumed by value**

Consuming a shared object by value permanently marks its clique as always hot. Because you cannot wrap shared objects (you must re-share or delete them), the system treats consuming one by value similarly to a hot potato that you can never resolve. A non-public `entry` function can receive a shared object by value directly, but it cannot receive a value whose clique previously interacted with a shared object consumed by value.

**Examples**

Consider the following module:

```move
module ex::m;

public struct HotPotato()

public fun hot<T>(x: &mut Coin<T>): HotPotato { ... }

entry fun spend<T>(x: &mut Coin<T>) { ... }

public fun cool(h: HotPotato) { ... }
```

In this invalid PTB, the `HotPotato` from command 0 is still alive in the same clique as `Input(0)` when command 1 calls `spend`:

```rust
// Invalid PTB
// Input 0: Coin<SUI>
// cliques: { Input(0) } => 0
0: ex::m::hot(Input(0));
// cliques: { Input(0), Result(0) } => 1
1: ex::m::spend(Input(0)); // INVALID: Input(0)'s clique has count > 0
2: ex::m::cool(Result(0));
```

However, if you consume the hot potato before calling `spend`, the clique's count drops to 0 and the call succeeds:

```rust
// Valid PTB
// Input 0: Coin<SUI>
// cliques: { Input(0) } => 0
0: ex::m::hot(Input(0));
// cliques: { Input(0), Result(0) } => 1
1: ex::m::cool(Result(0));
// cliques: { Input(0) } => 0
2: ex::m::spend(Input(0)); // Valid: Input(0)'s clique has count 0
```

In a flash loan scenario, entanglement extends transitively. Even if a value was not directly involved in the loan, being in the same clique as the loan's hot potato makes it ineligible for a non-public `entry` call:

```move
module flash::loan;

public struct Loan { amount: u64 }

public fun issue(bank: &mut Bank, amount: u64): (Balance<SUI>, Loan) { ... }

public fun repay(bank: &mut Bank, loan: Loan, repayment: Balance<SUI>) { ... }
```

```rust
// Invalid PTB
// Input 0: flash::loan::Bank, Input 1: u64
// cliques: { Input(0) } => 0, { Input(1) } => 0
0: flash::loan::issue(Input(0), Input(1))
// cliques: { Input(0), Input(1), NestedResult(0,0), NestedResult(0,1) } => 1
1: sui::coin::from_balance(NestedResult(0,0));
// cliques: { Input(0), Input(1), NestedResult(0,1), Result(1) } => 1
2: ex::m::spend(Result(1)); // INVALID: Result(1)'s clique has count > 0
3: sui::coin::into_balance(Result(1));
4: flash::loan::repay(Input(0), NestedResult(0,1), Result(3));
```

Even though the `Coin` created in command 1 was not directly involved in the flash loan, it is part of a clique with the outstanding `Loan` hot potato (`NestedResult(0,1)`). Repaying the loan before calling `spend` would make the PTB valid.

#### Pure value type checking

The system does not type check pure values until you use them. The first time a pure value appears as an argument expecting type `T`, the system checks that `T` is a valid pure type (see the [Inputs section](/develop/transactions/ptbs/inputs-and-results)) and that the bytes deserialize to `T`. Each distinct type creates a separate typed copy of the bytes, so you can use the same pure input at multiple types as long as the bytes are valid for each. For example, you can use the same bytes as an ASCII string `std::ascii::String` and as a UTF-8 string `std::string::String`. The system treats each typed copy as its own input, so mutations affect only that type's value, not all values created from the same bytes.

#### Publish and upgrade rules

Both commands embed their module bytes and dependency IDs directly in the command structure, however, these are not PTB `Argument` values.

**Publish:**

- Returns a single `sui::package::UpgradeCap`.
- After the runtime stages the package, it calls each module's `init` function (if present) in the order you provide the modules (in the vector of serialized bytes). `init` receives `&mut TxContext` and optionally a one-time witness. Additional `init` arguments are not yet supported but are planned.
- The package is available to `init` functions during execution. The runtime stages it before they run.

**Upgrade:**

- Takes exactly one PTB argument: a `sui::package::UpgradeTicket` (by value).
- Returns a single `sui::package::UpgradeReceipt`.
- Does not call `init` functions. Newly added modules in an upgrade cannot have `init` functions.
- The module digest and package ID in the ticket must match exactly.
- The runtime enforces the upgrade policy (compatible, additive, dep-only) from the ticket.

### End of execution

At the end of execution, the runtime checks the remaining values and calculates the effects for the transaction.

**For inputs:**

- The runtime skips remaining immutable or read only input objects (no modifications made).

- The runtime returns remaining mutable input objects to their original owners (shared remain shared, owned remain owned).

- The runtime drops remaining pure input values (all permissible types have `copy` and `drop`).

- The transaction can only delete or re-share shared objects. Any other operation (wrap, transfer, freezing) results in an error.

**For results:**

- The runtime drops remaining results with the `drop` ability.

- If a value has `copy` but not `drop`, its last usage must have been by-value (treated as a move).

- Otherwise, the runtime returns an error for unused values without `drop`.

**For gas:**

The runtime returns any remaining SUI deducted from the gas coin at the beginning of execution to the coin, even if the owner has changed. The runtime deducts the maximum possible gas at the beginning of execution and returns unused gas at the end (all in SUI). Only two commands can take the gas coin by value: `TransferObjects`, and a `MoveCall` to `sui::coin::send_funds`, which deposits the coin into an address balance and deletes the coin object. No other command can wrap or delete it.

The execution layer then passes out the total effects (created, mutated, and deleted objects), and the Sui network applies them.

## Execution example

By following each command's execution, you can see how inputs flow through commands, how results accumulate, and how the final transaction effects are determined.

Suppose you want to buy 2 items from a marketplace costing `100 MIST`. You keep one item for yourself and send the other item plus the remaining coin to a friend at address `0x808`.

```rust
{
  inputs: [
    Pure(/* @0x808 BCS bytes */ ...),
    Object(SharedObject { /* Marketplace shared object */ id: market_id, ... }),
    Pure(/* 100u64 BCS bytes */ ...),
  ]
  commands: [
    SplitCoins(GasCoin, [Input(2)]),
    MoveCall("some_package", "some_marketplace", "buy_two", [], [Input(1), NestedResult(0, 0)]),
    TransferObjects([GasCoin, NestedResult(1, 0)], Input(0)),
    MoveCall("sui", "tx_context", "sender", [], []),
    TransferObjects([NestedResult(1, 1)], NestedResult(3, 0)),
  ]
}
```

The inputs include the friend's address, the marketplace object, and the coin split value. The commands split off the coin, call the marketplace function, send the gas coin and one object, grab your address (through `sui::tx_context::sender`), and send the remaining object to yourself.

**Command 0: `SplitCoins(GasCoin, [Input(2)])`**

Accesses the gas coin by mutable reference and loads `Input(2)` as `100u64` (copied, not moved). Creates a new coin.

| State | Value |
| :---- | :---- |
| Gas coin balance | `499,900 MIST` |
| Result | `[Coin<SUI> { id: new_coin, value: 100 }]` |

**Command 1: `MoveCall("some_package", "some_marketplace", "buy_two", ...)`**

Calls `buy_two(marketplace: &mut Marketplace, coin: Coin<SUI>, ctx: &mut TxContext): (Item, Item)`. Uses `Input(1)` by mutable reference (marketplace) and `NestedResult(0, 0)` by value (coin is moved and consumed).

| State | Value |
| :---- | :---- |
| `Results[0][0]` | moved |
| Result | `[Item { id: id1 }, Item { id: id2 }]` |

**Command 2: `TransferObjects([GasCoin, NestedResult(1, 0)], Input(0))`**

Transfers the gas coin and first item to `0x808`. The command takes both objects by value (moves them).

| State | Value |
| :---- | :---- |
| Gas coin | moved to `0x808` (unused gas still returned to this coin at the end of execution) |
| `Results[1][0]` | moved |
| Result | `[]` |

**Command 3: `MoveCall("sui", "tx_context", "sender", [], [])`**

Calls `sender(ctx: &TxContext): address`. Returns the sender's address.

| State | Value |
| :---- | :---- |
| Result | `[sender_address]` |

**Command 4: `TransferObjects([NestedResult(1, 1)], NestedResult(3, 0))`**

Transfers the second item to the sender. The command moves the item by value and copies the address by value.

| State | Value |
| :---- | :---- |
| `Results[1][1]` | moved |
| Result | `[]` |

#### Initial state

```rust
Gas Coin: Coin<SUI> { id: gas_coin, balance: 1_000_000u64 }
Inputs: [Pure(@0x808), Marketplace { id: market_id }, Pure(100u64)]
Results: []
```

After the runtime deducts the maximum gas budget, which is `500_000` in this example:

```rust
Gas Coin: Coin<SUI> { id: gas_coin, balance: 500_000u64 }
```

#### Final state

```rust
Gas Coin: _ (moved)
Inputs: [Pure(@0x808), Marketplace { id: market_id } (mutated), Pure(100u64)]
Results: [
  [_],
  [_, _],
  [],
  [sender_address],
  []
]
```

#### End of execution checks

- Input objects: Gas coin moved. Marketplace remains shared (mutated).

- Results: All remaining values have `drop` ability (Pure inputs, sender's address). All other results moved.

- Shared objects: Marketplace not moved, remains shared.

**Transaction effects:**

- Coin split from gas (`new_coin`) does not appear (created and deleted in same transaction).

- Gas coin and `Item { id: id1 }` transferred to `0x808`. Remaining gas returned to the gas coin despite the owner change, so `0x808` receives the unused gas with the coin.

- `Item { id: id2 }` transferred to sender.

- Marketplace object returned as shared (mutated).

## What is address balance gas payment?

An [address balance](/onchain-finance/asset-custody/address-balances) is a balance of a coin type, such as SUI, that an address holds directly rather than in a `Coin<T>` object. For example, `sui::coin::send_funds` deposits a coin into the recipient's address balance and deletes the coin object. With normal gas payment, you list one or more `Coin<SUI>` objects in the gas payment, and the runtime combines (smashes) them into a single gas coin. With address balance gas payment, you leave the gas payment list empty, and the protocol deducts gas from the SUI address balance of the gas owner (the sender, or the sponsor in a sponsored transaction). In that case, no real coin object receives the final gas charge.

When the fullnode selects gas for you, it pays from the gas owner's address balance if the transaction never touches the gas coin and that balance covers the budget. Otherwise, it pays from their SUI coins. If the transaction does use the gas coin, it adds an address balance reservation so both sources are available.

## How do commands merge cliques?

All arguments to a single command, together with the command's results, merge into one clique. Specific arguments are not paired. A by-reference argument belongs to the same clique as the value it borrows and carries the same heat, so it merges cliques exactly like a by-value argument. `TxContext` is the exception: it never joins a clique. After values share a clique, they stay in it for the rest of the transaction.
