Skip to main content

Introduction to Flow Actions

warning

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.

  1. Source: Provides tokens on demand (e.g. withdraw from vault, claim rewards, pull liquidity)

source

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

sink

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

swapper

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

price oracle

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

flasher

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:


_10
import "FlowToken"
_10
_10
access(all) fun main(): String {
_10
return Type<@FlowToken.Vault>().identifier
_10
}

You'll get:


_10
A.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

info

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.

source

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:


_10
access(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:


_20
import "FungibleToken"
_20
import "FungibleTokenConnectors"
_20
_20
transaction {
_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.

sink

Sinks adhere to the Sink interface.


_10
access(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:


_27
import "FungibleToken"
_27
import "FungibleTokenConnectors"
_27
_27
transaction {
_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.

swapper

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:


_13
access(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:


_33
import "FlowToken"
_33
import "USDCFlow"
_33
import "IncrementFiSwapConnectors"
_33
import "SwapConfig"
_33
_33
transaction {
_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.

price oracle

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:


_10
access(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:

info

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.


_32
import "FlowToken"
_32
import "FungibleToken"
_32
import "FungibleTokenConnectors"
_32
import "BandOracleConnectors"
_32
_32
transaction {
_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.

flasher

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:

  1. Borrow 1 million dollars
  2. Purchase the NFT
  3. Sell the NFT
  4. Repay 1 million dollars plus a small fee
warning

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:


_13
access(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:


_62
import "FungibleToken"
_62
import "FlowToken"
_62
import "USDCFlow"
_62
import "SwapInterfaces"
_62
import "SwapConfig"
_62
import "SwapFactory"
_62
import "IncrementFiFlashloanConnectors"
_62
_62
transaction {
_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
_62
access(all)
_62
fun 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:


_82
import "FungibleToken"
_82
import "FlowToken"
_82
import "USDCFlow"
_82
import "FungibleTokenConnectors"
_82
import "IncrementFiSwapConnectors"
_82
import "SwapConfig"
_82
import "DeFiActions"
_82
_82
transaction {
_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