Introduction to Flow Actions
Flow Actions are being reviewed and finalized in FLIP 339. The specific implementation may change as a part of this process.
These tutorials will be updated, but you may need to refactor your code if the implementation changes.
Actions are a suite of standardized Cadence interfaces that enable developers to compose complex workflows, starting with DeFi, by connecting small, reusable components. Actions provide a "LEGO" framework of plug-and-play blocks where each component performs a single operation (deposit, withdraw, swap, price lookup, flash loan) while maintaining composability with other components to create sophisticated workflows executable in a single atomic transaction.
By using Flow Actions, developers are to able remove large amounts of bespoke 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'll be able to follow along, but you'll get more out of it if you complete our series of Cadence tutorials. Most developers find it more pleasant 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 (e.g. withdraw from vault, claim rewards, pull liquidity)

- Sink: Accepts tokens up to capacity (e.g. deposit to vault, repay loan, add liquidity)

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

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

- Flasher: Provides flash loans with atomic repayment (e.g. arbitrage, liquidations)

Connectors
Connectors create the bridge between the standardized interfaces of Flow Actions and the often bespoke and complicated mechanisms of different DeFi protocols. You can utilize existing connectors written by other developers, or create your own.
Flow Actions are instantiated by creating an instance of the appropriate [struct] from a connector that provides the desired type of action connected to the desired DeFi protocol.
Read the connectors article to learn more about them.
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
These types are used by many Flow Actions 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 ai agents - can more easily write transactions and functionality regardless of the myriad of different ways each protocol works to accomplish these tasks.
That being said, 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 containing the requested type and amount of tokens from something the user controls, or has authorized access to. This includes, but is not 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}
In other words, every source is guaranteed to have the above functions and return types allowing you to get the type of vault returned by the source, get an estimate of how many tokens may be withdrawn currently, 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.
You create a source by instantiating a struct that conforms to the Source interface corresponding to a given protocol connector. For example, if you want to create a source from a generic vault, you can do that by creating 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 are left in the vault provided by the source. 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, by instantiating 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 in the event that 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] enables 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}
Once again, you create a swapper by instantiating 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 solve this problem by enabling 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 be left having 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 enables protocols to trace stack operations via Flow Actions interface-level events, identifying them by IDs. IdentifiableResource implementations should ensure that access to the identifier is encapsulated by the structures they identify.
While Cadence struct types can be created in any context (including being passed in as transaction parameters), the authorized AuthenticationToken capability ensures that only those issued by the Flow Actions contract can be utilized 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 (e.g., 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 (e.g., 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
- Enables 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 should 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