Introduction to Flow Actions
Actions are a suite of standardized Cadence interfaces that allow developers to compose complex workflows, starting with decentralized finance (DeFi) workflows, by connecting small, reusable components. Actions provide a "LEGO" framework of blocks where each component performs a single operation (deposit, withdraw, swap, price lookup, flash loan) while maintaining composability with other components. This creates sophisticated workflows executable in a single atomic transaction.
By using Flow Actions, developers can remove large amounts of tailored complexity from building DeFi apps and can instead focus on business logic using nouns and verbs.
Key Features
- Atomic Composition - All operations complete or fail together.
- Weak Guarantees - Flexible error handling, no-ops when conditions aren't met.
- Event Traceability - UniqueIdentifier system for tracking operations.
- Protocol Agnostic - Standardized interfaces across different protocols.
- Struct-based - Lightweight, copyable components for efficient composition.
Learning Objectives
After completing this tutorial, you will be able to:
- Understand the key features of Flow Actions including atomic composition, weak guarantees, and event traceability
- Create and use Sources to provide tokens from various protocols and locations
- Create and use Sinks to accept tokens up to defined capacity limits
- Create and use Swappers to exchange tokens between different types with price estimation
- Create and use Price Oracles to get price data for assets with consistent denomination
- Create and use Flashers to provide flash loans with atomic repayment requirements
- Use UniqueIdentifiers to trace and correlate operations across multiple Flow Actions
- Compose complex DeFi workflows by connecting multiple Actions in a single atomic transaction
Prerequisites
Cadence Programming Language
This tutorial assumes you have a modest knowledge of Cadence. If you don't, you can follow along, but you'll get more out of it if you complete our Cadence tutorials. Most developers find it easier than other blockchain languages and it's not hard to pick up.
Flow Action Types
The first five Flow Actions implement five core primitives to integrate external DeFi protocols.
- Source: Provides tokens on demand (for example, withdraw from vault, claim rewards, pull liquidity)

- Sink: Accepts tokens up to capacity (for example, deposit to vault, repay loan, add liquidity)

- Swapper: Exchanges one token type for another (for example, targeted DEX trades, multi-protocol aggregated swaps)

- PriceOracle: Provides price data for assets (for example, external price feeds, DEX prices, price caching)

- Flasher: Provides flash loans with atomic repayment (for example, arbitrage, liquidations)

Connectors
Connectors create the bridge between the standardized interfaces of Flow Actions and the often customized and complicated mechanisms of different DeFi protocols. You can use existing connectors that other developers wrote, or create your own.
To instantiate Flow Actions, create an instance of the appropriate [struct] from a connector that provides the desired type of action connected to the desired DeFi protocol.
For more information, read the connectors article.
Token Types
In Cadence, tokens that adhere to the Fungible Token Standard have types that work with type safety principles.
For example, you can find the type of $FLOW by running this script:
_10import "FlowToken"_10_10access(all) fun main(): String {_10    return Type<@FlowToken.Vault>().identifier_10}
You'll get:
_10A.1654653399040a61.FlowToken.Vault
Many Flow Actions use these types to provide a safer method of working with tokens than an arbitrary address that may or may not be a token.
Flow Actions
The following Flow Actions standardize usage patterns for common defi-related tasks. By working with them, you - or Artificial Intelligence (AI) agents - can more easily write transactions and functionality regardless of the myriad of different ways each protocol works to accomplish these tasks.
Defi protocols and tools operate very differently, which means the calls to instantiate the same kind of action connected to different protocols will vary by protocol and connector.
Source
A source is a primitive component that can supply a vault which contains the requested type and amount of tokens from something the user controls, or has authorized access to. This includes, but isn't limited to, personal vaults, accounts in protocols, and rewards.

You'll likely use one or more sources in any transactions using actions if the user needs to pay for something or otherwise provide tokens.
Sources conform to the Source interface:
_10access(all) struct interface Source : IdentifiableStruct {_10    /// Returns the Vault type provided by this Source_10    access(all) view fun getSourceType(): Type_10    /// Returns an estimate of how much can be withdrawn_10    access(all) fun minimumAvailable(): UFix64_10    /// Withdraws up to maxAmount, returning what's actually available_10    access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault}_10}
Every source is guaranteed to have the above functions and return types that allow you to get the type of vault that the source returns, get an estimate of how many tokens users may currently withdraw, and actually withdraw those tokens, up to the amount available.
Sources degrade gracefully - If the requested amount of tokens is not available, they return the available amount. They always return a vault, even if that vault is empty.
To create a source, instantiate a struct that conforms to the Source interface corresponding to a given protocol connector. For example, to create a source from a generic vault, create a VaultSource from FungibleTokenConnectors:
_20import "FungibleToken"_20import "FungibleTokenConnectors"_20_20transaction {_20_20  prepare(acct: auth(BorrowValue) {_20    let withdrawCap = acct.storage.borrow<auth(FungibleToken.Withdraw) {FungibleToken.Vault}>(_20      /storage/flowTokenVault_20    )_20_20    let source = FungibleTokenConnectors.VaultSource(_20      min: 0.0,_20      withdrawVault: withdrawCap,_20      uniqueID: nil_20    )_20_20    // Note: Logs are only visible in the emulator console_20    log("Source created for vault type: ".concat(source.withdrawVaultType.identifier))_20  }_20}
Sink
A sink is the opposite of a source - it's a place to send tokens, up to the limit of the capacity defined in the sink. As with any resource, this process is non-destructive. Any remaining tokens remain in the vault that the source provides. They also have flexible limits, meaning the capacity can be dynamic.

Sinks adhere to the Sink interface.
_10access(all) struct interface Sink : IdentifiableStruct {_10    /// Returns the Vault type accepted by this Sink_10    access(all) view fun getSinkType(): Type_10    /// Returns an estimate of remaining capacity_10    access(all) fun minimumCapacity(): UFix64_10    /// Deposits up to capacity, leaving remainder in the referenced vault_10    access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault})_10}
You create a sink similar how you create a source, which is to instantiate an instance of the appropriate struct from the connector. For example, to create a sink in a generic vault from, instantiate a VaultSink from FungibleTokenConnectors:
_27import "FungibleToken"_27import "FungibleTokenConnectors"_27_27transaction {_27_27  prepare(acct: &Account) {_27    // Public, non-auth capability to deposit into the vault_27    let depositCap = acct.capabilities.get<&{FungibleToken.Vault}>(_27      /public/flowTokenReceiver_27    )_27_27    // Optional: specify a max balance the user's Flow Token vault should hold_27    let maxBalance: UFix64? = nil // or UFix64(1000.0)_27_27    // Optional: for aligning with Source in a stack_27    let uniqueID = nil_27_27    let sink = FungibleTokenConnectors.VaultSink(_27      max: maxBalance,_27      depositVault: depositCap,_27      uniqueID: uniqueID_27    )_27_27    // Note: Logs are only visible in the emulator console_27    log("VaultSink created for deposit type: ".concat(sink.depositVaultType.identifier))_27  }_27}
Swapper
A swapper exchanges tokens between different types with support for bidirectional swaps and price estimation. Bi-directional means that they support swaps in both directions, which is necessary if an inner connector can't accept the full swap output balance.

They also contain price discovery to provide estimates for the amounts in and out via the [{Quote}] object, and the [quote system] allows price caching and execution parameter optimization.
Swappers conform to the Swapper interface:
_13access(all) struct interface Swapper : IdentifiableStruct {_13    /// Input and output token types - in and out token types via default `swap()` route_13    access(all) view fun inType(): Type_13    access(all) view fun outType(): Type_13_13    /// Price estimation methods - quote required amount given some desired output & output for some provided input_13    access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {Quote}_13    access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {Quote}_13_13    /// Swap execution methods_13    access(all) fun swap(quote: {Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault}_13    access(all) fun swapBack(quote: {Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault}_13}
To create a swapper, instantiate the appropriate struct from the appropriate connector. To create a swapper for IncrementFi with the IncrementFiSwapConnectors, instantiate Swapper:
_33import "FlowToken"_33import "USDCFlow"_33import "IncrementFiSwapConnectors"_33import "SwapConfig"_33_33transaction {_33  prepare(acct: &Account) {_33    // Derive the path keys from the token types_33    let flowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: Type<@FlowToken.Vault>().identifier)_33    let usdcFlowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: Type<@USDCFlow.Vault>().identifier)_33_33    // Minimal path Flow -> USDCFlow_33    let swapper = IncrementFiSwapConnectors.Swapper(_33      path: [_33        flowKey,_33        usdcFlowKey_33      ],_33      inVault: Type<@FlowToken.Vault>(),_33      outVault: Type<@USDCFlow.Vault>(),_33      uniqueID: nil_33    )_33_33    // Example: quote how much USDCFlow you'd get for 10.0 FLOW_33    let qOut = swapper.quoteOut(forProvided: 10.0, reverse: false)_33    // Note: Logs are only visible in the emulator console_33    log(qOut)_33_33    // Example: quote how much FLOW you'd need to get 25.0 USDCFlow_33    let qIn = swapper.quoteIn(forDesired: 25.0, reverse: false)_33    // Note: Logs are only visible in the emulator console_33    log(qIn)_33  }_33}
Price Oracle
A price oracle provides price data for assets with a consistent denomination. All prices are returned in the same unit and will return nil rather than reverting in the event that a price is unavailable. Prices are indexed by Cadence type, requiring a specific Cadence-based token type for which to serve prices, as opposed to looking up an asset by a generic address.

You can pass an argument this Type, or any conforming fungible token type conforming to the interface to the price function to get a price.
The full interface for PriceOracle is:
_10access(all) struct interface PriceOracle : IdentifiableStruct {_10    /// Returns the denomination asset (e.g., USDCf, FLOW)_10    access(all) view fun unitOfAccount(): Type_10    /// Returns current price or nil if unavailable, conditions for which are implementation-specific_10    access(all) fun price(ofToken: Type): UFix64?_10}
To create a PriceOracle from Band with BandOracleConnectors:
You need to pay the oracle to get information from it. Here, we're using another Flow Action - a source - to fund getting a price from the oracle.
_32import "FlowToken"_32import "FungibleToken"_32import "FungibleTokenConnectors"_32import "BandOracleConnectors"_32_32transaction {_32_32  prepare(acct: auth(IssueStorageCapabilityController) &Account) {_32    // Ensure we have an authorized capability for FlowToken (auth Withdraw)_32    let storagePath = /storage/flowTokenVault_32    let withdrawCap = acct.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(storagePath)_32_32    // Fee source must PROVIDE FlowToken vaults (per PriceOracle preconditions)_32    let feeSource = FungibleTokenConnectors.VaultSource(_32      min: 0.0,                   // keep at least 0.0 FLOW in the vault_32      withdrawVault: withdrawCap, // auth withdraw capability_32      uniqueID: nil_32    )_32_32    // unitOfAccount must be a mapped symbol in BandOracleConnectors.assetSymbols._32    // The contract's init already maps FlowToken -> "FLOW", so this is valid._32    let oracle = BandOracleConnectors.PriceOracle(_32      unitOfAccount: Type<@FlowToken.Vault>(), // quote token (e.g. FLOW in BASE/FLOW)_32      staleThreshold: 600,                     // seconds; nil to skip staleness checks_32      feeSource: feeSource,_32      uniqueID: nil_32    )_32_32    // Note: Logs are only visible in the emulator console_32    log("Created PriceOracle; unit: ".concat(oracle.unitOfAccount().identifier))_32  }_32}
Flasher
A flasher provides flash loans with atomic repayment requirements.

If you're not familiar with flash loans, imagine a scenario where you discovered an NFT listed for sale one one marketplace for 1 million dollars, then noticed an open bid to buy that same NFT for 1.1 million dollars on another marketplace.
In theory, you could make an easy 100k by buying the NFT on the first marketplace and then fulfilling the open buy offer on the second marketplace. There's just one big problem - You might not have 1 million dollars liquid just laying around for you to purchase the NFT!
Flash loans allow you to create one transaction during which you:
- Borrow 1 million dollars.
- Purchase the NFT.
- Sell the NFT.
- Repay 1 million dollars plus a small fee.
This scenario may be a scam. A scammer could set up this situation as bait and cancel the buy order the instant someone purchases the NFT that is for sale. You'd have paid a vast amount of money for something worthless.
The great thing about Cadence transactions, with or without Actions, is that you can set up an atomic transaction where everything either works, or is reverted. Either you make 100k, or nothing happens except a tiny expenditure of gas.
Flashers adhere to the Flasher interface:
_13access(all) struct interface Flasher : IdentifiableStruct {_13    /// Returns the asset type this Flasher can issue as a flash loan_13    access(all) view fun borrowType(): Type_13    /// Returns the estimated fee for a flash loan of the specified amount_13    access(all) fun calculateFee(loanAmount: UFix64): UFix64_13    /// Performs a flash loan of the specified amount. The callback function is passed the fee amount, a loan Vault,_13    /// and data. The callback function should return a Vault containing the loan + fee._13    access(all) fun flashLoan(_13        amount: UFix64,_13        data: AnyStruct?,_13        callback: fun(UFix64, @{FungibleToken.Vault}, AnyStruct?): @{FungibleToken.Vault} // fee, loan, data_13    )_13}
You create a flasher the same way as the other actions, but you'll need the address for a SwapPair. You can get that onchain at runtime. For example, to borrow $FLOW from IncrementFi:
_62import "FungibleToken"_62import "FlowToken"_62import "USDCFlow"_62import "SwapInterfaces"_62import "SwapConfig"_62import "SwapFactory"_62import "IncrementFiFlashloanConnectors"_62_62transaction {_62_62  prepare(_ acct: &Account) {_62    // Increment uses token *keys* like "A.1654653399040a61.FlowToken" (mainnet FlowToken)_62    // and "A.f1ab99c82dee3526.USDCFlow" (mainnet USDCFlow)._62    let flowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: Type<@FlowToken.Vault>().identifier)_62    let usdcFlowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: Type<@USDCFlow.Vault>().identifier)_62_62    // Ask the factory for the pair's public capability (or address), then verify it._62    // Depending on the exact factory interface you have, one of these will exist:_62    //   - getPairAddress(token0Key: String, token1Key: String): Address_62    //   - getPairPublicCap(token0Key: String, token1Key: String): Capability<&{SwapInterfaces.PairPublic}>_62    //   - getPair(token0Key: String, token1Key: String): Address_62    //_62    // Try address first; if your factory exposes a different helper, swap it in._62    let pairAddr: Address = SwapFactory.getPairAddress(flowKey, usdcFlowKey)_62_62    // Sanity-check: borrow PairPublic and verify it actually contains FLOW/USDCFlow_62    let pair = getAccount(pairAddr)_62      .capabilities_62      .borrow<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath)_62      ?? panic("Could not borrow PairPublic at resolved address")_62_62    let info = pair.getPairInfoStruct()_62    assert(_62      (info.token0Key == flowKey && info.token1Key == usdcFlowKey) ||_62      (info.token0Key == usdcFlowKey && info.token1Key == flowKey),_62      message: "Resolved pair does not match FLOW/USDCFlow"_62    )_62_62    // Instantiate the Flasher to borrow FLOW (switch to USDCFlow if you want that leg)_62    let flasher = IncrementFiFlashloanConnectors.Flasher(_62      pairAddress: pairAddr,_62      type: Type<@FlowToken.Vault>(),_62      uniqueID: nil_62    )_62_62    // Note: Logs are only visible in the emulator console_62    log("Flasher ready on mainnet FLOW/USDCFlow at ".concat(pairAddr.toString()))_62_62    flasher.flashloan(_62      amount: 100.0_62      data: nil_62      callback: flashloanCallback_62    )_62  }_62}_62_62// Callback function passed to flasher.flashloan_62access(all)_62fun flashloanCallback(fee: UFix64, loan: @{FungibleToken.Vault}, data: AnyStruct?): @{FungibleToken.Vault} {_62  log("Flashloan with balance of \(loan.balance) \(loan.getType().identifier) executed")_62  return <-loan_62}
Identification and Traceability
The UniqueIdentifier allows protocols to trace stack operations via Flow Actions interface-level events, identifying them by IDs. IdentifiableResource implementations should verify that access to the identifier is encapsulated by the structures they identify.
While you can create Cadence struct types in any context (including being passed in as transaction parameters), the authorized AuthenticationToken capability verifies that only those issued by the Flow Actions contract can be used in connectors, preventing forgery.
For example, to use a UniqueIdentifier in a source->swap->sink:
_82import "FungibleToken"_82import "FlowToken"_82import "USDCFlow"_82import "FungibleTokenConnectors"_82import "IncrementFiSwapConnectors"_82import "SwapConfig"_82import "DeFiActions"_82_82transaction {_82_82  prepare(acct: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {_82    // Standard token paths_82    let storagePath = /storage/flowTokenVault_82    let receiverStoragePath = USDCFlow.VaultStoragePath_82    let receiverPublicPath = USDCFlow.VaultPublicPath_82_82    // Ensure private auth-withdraw (for Source)_82    let withdrawCap = acct.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(storagePath)_82_82    // Ensure public receiver Capability (for Sink) - configure receiving Vault is none exists_82    if acct.storage.type(at: receiverStoragePath) == nil {_82      // Save the USDCFlow Vault_82      acct.storage.save(<-USDCFlow.createEmptyVault(vaultType: Type<@USDCFlow.Vault>()), to: USDCFlow.VaultStoragePath)_82      // Issue and publish public Capabilities to the token's default paths_82      let publicCap = acct.capabilities.storage.issue<&USDCFlow.Vault>(storagePath)_82        ?? panic("failed to link public receiver")_82      acct.capabilities.unpublish(receiverPublicPath)_82      acct.capabilities.unpublish(USDCFlow.ReceiverPublicPath)_82      acct.capabilities.publish(cap, at: receiverPublicPath)_82      acct.capabilities.publish(cap, at: USDCFlow.ReceiverPublicPath)_82    }_82    let depositCap = acct.capabilities.get<&{FungibleToken.Vault}>(receiverPublicPath)_82_82    // Initialize shared UniqueIdentifier - passed to each connector on init_82    let uniqueIdentifier = DeFiActions.createUniqueIdentifier()_82_82    // Instantiate: Source, Swapper, Sink_82    let source = FungibleTokenConnectors.VaultSource(_82      min: 5.0,_82      withdrawVault: withdrawCap,_82      uniqueID: uniqueIdentifier_82    )_82_82    // Derive the IncrementFi token keys from the token types_82    let flowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: Type<@FlowToken.Vault>().identifier)_82    let usdcFlowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: Type<@USDCFlow.Vault>().identifier)_82_82    // Replace with a real Increment path when swapping tokens (e.g., FLOW → USDCFlow)_82    // e.g. ["A.1654653399040a61.FlowToken", "A.f1ab99c82dee3526.USDCFlow"]_82    let swapper = IncrementFiSwapConnectors.Swapper(_82      path: [flowKey, usdcFlowKey],_82      inVault: Type<@FlowToken.Vault>(),_82      outVault: Type<@USDCFlow.Vault>(),_82      uniqueID: uniqueIdentifier_82    )_82_82    let sink = FungibleTokenConnectors.VaultSink(_82      max: nil,_82      depositVault: depositCap,_82      uniqueID: uniqueIdentifier_82    )_82_82    // ----- Real composition (no destroy) -----_82    // 1) Withdraw from Source_82    let tokens <- source.withdrawAvailable(maxAmount: 100.0)_82_82    // 2) Swap with Swapper from FLOW → USDCFlow_82    let swapped <- swapper.swap(quote: nil, inVault: <-tokens)_82_82    // 3) Deposit into Sink (consumes by reference via withdraw())_82    sink.depositCapacity(from: &swapped as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})_82_82    // 4) Return any residual by depositing the *entire* vault back to user's USDCFlow vault_82    //    (works even if balance is 0; deposit will still consume the resource)_82    depositCap.borrow().deposit(from: <-swapped)_82_82    // Optional: inspect that all three share the same ID_82    log(source.id())_82    log(swapper.id())_82    log(sink.id())_82  }_82}
Why UniqueIdentifier Matters in FlowActions
The UniqueIdentifier is used to tag multiple FlowActions connectors as part of the same logical operation.
By aligning the same ID across connectors (for example, Source → Swapper → Sink), you can:
1. Event Correlation
- Every connector emits events tagged with its UniqueIdentifier.
- Shared IDs let you filter and group related events in the chain's event stream.
- Makes it easy to see that a withdrawal, swap, and deposit were part of one workflow.
2. Stack Tracing
- When using composite connectors (for example, SwapSource,SwapSink,MultiSwapper), IDs allow you to trace the complete path through the stack.
- Helpful for debugging and understanding the flow of operations inside complex strategies.
3. Analytics & Attribution
- Allows measuring usage of specific strategies or routes.
- Lets you join data from multiple connectors into a single logical "transaction" for reporting.
- Supports fee attribution and performance monitoring across multi-step workflows.
Without a Shared UniqueIdentifier
- Events from different connectors appear unrelated, even if they occurred in the same transaction.
- Harder to debug, track, or analyze multi-step processes.
Conclusion
In this tutorial, you learned about Flow Actions, a suite of standardized Cadence interfaces that enable developers to compose complex DeFi workflows using small, reusable components. You explored the five core Flow Action types - Source, Sink, Swapper, PriceOracle, and Flasher - and learned how to create and use them with various connectors.
Now that you have completed this tutorial, you can:
- Understand the key features of Flow Actions including atomic composition, weak guarantees, and event traceability
- Create and use Sources to provide tokens from various protocols and locations
- Create and use Sinks to accept tokens up to defined capacity limits
- Create and use Swappers to exchange tokens between different types with price estimation
- Create and use Price Oracles to get price data for assets with consistent denomination
- Create and use Flashers to provide flash loans with atomic repayment requirements
- Use UniqueIdentifiers to trace and correlate operations across multiple Flow Actions
- Compose complex DeFi workflows by connecting multiple Actions in a single atomic transaction