Composing Workflows with 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.
Flow Actions are designed to be composable meaning you can chain them together like LEGO blocks to build complex strategies. Each primitive has a standardized interface that works consistently across all protocols, eliminating the need to learn multiple APIs. This composability enables atomic execution of multi-step workflows within single transactions, ensuring either complete success or safe failure. By combining these primitives, developers can create sophisticated DeFi strategies like automated yield farming, cross-protocol arbitrage, and portfolio rebalancing. The 5 Flow Actions Primitives are:
-
Source → Provides tokens on demand by withdrawing from vaults or claiming rewards. Sources respect minimum balance constraints and return empty vaults gracefully when nothing is available.
-
Sink → Accepts token deposits up to a specified capacity limit. Sinks perform no-ops when capacity is exceeded rather than reverting, enabling smooth workflow execution.
-
Swapper → Exchanges one token type for another through DEX trades or cross-chain bridges. Swappers support bidirectional operations and provide quote estimation for slippage protection.
-
PriceOracle → Provides real-time price data for assets from external feeds or DEX prices. Oracles handle staleness validation and return nil for unavailable prices rather than failing.
-
Flasher → Issues flash loans that must be repaid within the same transaction via callback execution. Flashers enable capital-efficient strategies like arbitrage and liquidations without requiring upfront capital.
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
Core Flow Patterns
Linear Flow (Source → Swapper → Sink)
The most common pattern: get tokens, convert them, then deposit them.
Example: Claim rewards → Swap to different token → Stake in new pool
Bidirectional Flow (Source ↔ Sink)
Two-way operations where you can both deposit and withdraw.
Example: Vault operations with both deposit and withdrawal capabilities
Aggregated Flow (Multiple Sources → Aggregator → Sink)
Combine multiple sources for optimal results.
_10Source A → Aggregator → Sink_10Source B ↗_10Source C ↗
Example: Multiple DEX aggregators finding the best swap route
Common DeFi Workflow Combinations
Single Token to LP (Zapper)
Goal: Convert a single token into liquidity provider (LP) tokens in one transaction
The Zapper is a specialized connector that combines swapper and sink functionality. It takes a single token input and outputs LP tokens by automatically handling the token splitting, swapping, and liquidity provision process.
How it works:
- Takes single token A as input
- Splits it into two portions
- Swaps one portion to token B
- Provides liquidity with A + B to get LP tokens
- Returns LP tokens as output
_13// Zapper: Convert single FLOW token to FLOW/USDC LP tokens_13let zapper = IncrementFiPoolLiquidityConnectors.Zapper(_13 token0Type: Type<@FlowToken.Vault>(), // Input token type_13 token1Type: Type<@USDC.Vault>(), // Paired token type_13 stableMode: false, // Use volatile pricing_13 uniqueID: nil_13)_13_13// Execute: Input 100 FLOW → Output FLOW/USDC LP tokens_13let flowTokens <- flowVault.withdraw(amount: 100.0)_13let lpTokens <- zapper.swap(nil, inVault: <-flowTokens)_13_13// Now you have LP tokens ready for staking or further use
Benefits:
- Simplicity: Single transaction converts any token to LP position
- Efficiency: Automatically calculates optimal split ratios
- Composability: Output LP tokens work with any sink connector
Reward Harvesting & Conversion
Goal: Claim staking rewards and convert them to a stable token
This workflow automatically claims accumulated staking rewards and converts them to a stable asset like USDC. It combines a rewards source, token swapper, and vault sink to create a seamless reward collection and conversion process.
How it works:
- Claims pending rewards from a staking pool using user certificate
- Swaps the reward tokens (e.g., FLOW) to stable tokens (e.g., USDC)
- Deposits the stable tokens to a vault with capacity limits
- Returns any unconverted tokens back to the user
_28// 1. Source: Claim rewards from staking pool_28let rewardsSource = IncrementFiStakingConnectors.PoolRewardsSource(_28 userCertificate: userCert,_28 poolID: 1,_28 vaultType: Type<@FlowToken.Vault>(),_28 overflowSinks: {},_28 uniqueID: nil_28)_28_28// 2. Swapper: Convert rewards to stable token_28let swapper = IncrementFiSwapConnectors.Swapper(_28 path: ["A.FlowToken", "A.USDC"],_28 inVault: Type<@FlowToken.Vault>(),_28 outVault: Type<@USDC.Vault>(),_28 uniqueID: nil_28)_28_28// 3. Sink: Deposit stable tokens to vault_28let vaultSink = FungibleTokenConnectors.VaultSink(_28 max: 1000.0,_28 depositVault: vaultCap,_28 uniqueID: nil_28)_28_28// Execute the workflow_28let rewards = rewardsSource.withdrawAvailable(1000.0)_28let stableTokens = swapper.swap(nil, inVault: <-rewards)_28vaultSink.depositCapacity(from: &stableTokens)
Benefits:
- Risk Reduction: Converts volatile reward tokens to stable assets
- Automation: Single transaction handles claim, swap, and storage
- Capital Efficiency: No manual intervention needed for reward management
Liquidity Provision & Yield Farming
Goal: Convert single token to LP tokens for yield farming
This workflow takes a single token from your vault, converts it into liquidity provider (LP) tokens, and immediately stakes them for yield farming rewards. It combines vault operations, zapping functionality, and staking in one seamless transaction.
How it works:
- Withdraws single token (e.g., FLOW) from vault with minimum balance protection
- Uses Zapper to split token and create LP position (FLOW/USDC pair)
- Stakes the resulting LP tokens in a yield farming pool
- Begins earning rewards on the staked LP position
_26// 1. Source: Provide single token (e.g., FLOW)_26let flowSource = FungibleTokenConnectors.VaultSource(_26 min: 100.0,_26 withdrawVault: flowVaultCap,_26 uniqueID: nil_26)_26_26// 2. Zapper: Convert to LP tokens_26let zapper = IncrementFiPoolLiquidityConnectors.Zapper(_26 token0Type: Type<@FlowToken.Vault>(),_26 token1Type: Type<@USDC.Vault>(),_26 stableMode: false,_26 uniqueID: nil_26)_26_26// 3. Sink: Stake LP tokens for rewards_26let stakingSink = IncrementFiStakingConnectors.PoolSink(_26 staker: user.address,_26 poolID: 2,_26 uniqueID: nil_26)_26_26// Execute the workflow_26let flowTokens = flowSource.withdrawAvailable(100.0)_26let lpTokens = zapper.swap(nil, inVault: <-flowTokens)_26stakingSink.depositCapacity(from: &lpTokens)
Benefits:
- Yield Optimization: Converts idle tokens to yield-generating LP positions
- Single Transaction: No need for multiple manual steps or approvals
- Automatic Staking: LP tokens immediately start earning rewards
Cross-VM Bridge & Swap
Goal: Bridge tokens from Cadence to EVM, swap them, then bridge back
This workflow demonstrates Flow's unique cross-VM capabilities by bridging tokens from Cadence to Flow EVM, executing a swap using UniswapV2-style routing, and bridging the results back to Cadence. This enables access to EVM-based DEX liquidity while maintaining Cadence token ownership.
How it works:
- Withdraws tokens from Cadence vault with minimum balance protection
- Bridges tokens from Cadence to Flow EVM environment
- Executes swap using UniswapV2 router on EVM side
- Bridges the swapped tokens back to Cadence environment
- Deposits final tokens to target Cadence vault
_28// 1. Source: Cadence vault_28let cadenceSource = FungibleTokenConnectors.VaultSource(_28 min: 50.0,_28 withdrawVault: cadenceVaultCap,_28 uniqueID: nil_28)_28_28// 2. EVM Swapper: Cross-VM swap_28let evmSwapper = UniswapV2SwapConnectors.Swapper(_28 routerAddress: EVM.EVMAddress(0x...),_28 path: [tokenA, tokenB],_28 inVault: Type<@FlowToken.Vault>(),_28 outVault: Type<@USDC.Vault>(),_28 coaCapability: coaCap,_28 uniqueID: nil_28)_28_28// 3. Sink: Cadence vault for swapped tokens_28let cadenceSink = FungibleTokenConnectors.VaultSink(_28 max: nil,_28 depositVault: usdcVaultCap,_28 uniqueID: nil_28)_28_28// Execute the workflow_28let cadenceTokens = cadenceSource.withdrawAvailable(50.0)_28let evmTokens = evmSwapper.swap(nil, inVault: <-cadenceTokens)_28cadenceSink.depositCapacity(from: &evmTokens)
Benefits:
- Extended Liquidity: Access to both Cadence and EVM DEX liquidity
- Cross-VM Arbitrage: Exploit price differences between VM environments
- Atomic Execution: All bridging and swapping happens in single transaction
Flash Loan Arbitrage
Goal: Borrow tokens, execute arbitrage, repay loan with profit
This advanced strategy uses flash loans to execute risk-free arbitrage by borrowing tokens, exploiting price differences across multiple DEXs, and repaying the loan with interest while keeping the profit. The entire operation happens atomically within a single transaction.
How it works:
- Borrows tokens via flash loan without collateral requirements
- Uses multi-swapper to find optimal arbitrage routes across DEXs
- Executes trades to exploit price differences
- Repays flash loan with fees from arbitrage profits
- Keeps remaining profit after loan repayment
_17// 1. Flasher: Borrow tokens for arbitrage_17let flasher = IncrementFiFlashloanConnectors.Flasher(_17 pairAddress: pairAddress,_17 type: Type<@FlowToken.Vault>(),_17 uniqueID: nil_17)_17_17// 2. Multi-swapper: Find best arbitrage route_17let multiSwapper = SwapConnectors.MultiSwapper(_17 inVault: Type<@FlowToken.Vault>(),_17 outVault: Type<@FlowToken.Vault>(),_17 swappers: [swapper1, swapper2, swapper3],_17 uniqueID: nil_17)_17_17// 3. Execute arbitrage with callback_17flasher.flashLoan(1000.0, callback: arbitrageCallback)
Benefits:
- Zero Capital Required: No upfront investment needed for arbitrage
- Risk-Free Profit: Transaction reverts if arbitrage isn't profitable
- Market Efficiency: Helps eliminate price discrepancies across DEXs
Advanced Workflow Combinations
Vault Source + Zapper Integration
Goal: Withdraw tokens from a vault and convert them to LP tokens in a single transaction
This advanced workflow demonstrates the power of combining VaultSource with Zapper functionality to seamlessly convert idle vault tokens into yield-generating LP positions. The Zapper handles the complex process of splitting the single token and creating balanced liquidity.
How it works:
- VaultSource withdraws tokens from vault while respecting minimum balance
- Zapper receives the single token and splits it optimally
- Zapper swaps a portion of token A to token B using internal DEX routing
- Zapper provides balanced liquidity (A + B) to the pool
- Returns LP tokens that represent the liquidity position
_21// 1. Create VaultSource with minimum balance protection_21let vaultSource = FungibleTokenConnectors.VaultSource(_21 min: 500.0, // Keep 500 tokens minimum in vault_21 withdrawVault: flowVaultCapability,_21 uniqueID: nil_21)_21_21// 2. Create Zapper for FLOW/USDC pair_21let zapper = IncrementFiPoolLiquidityConnectors.Zapper(_21 token0Type: Type<@FlowToken.Vault>(), // Input token (A)_21 token1Type: Type<@USDC.Vault>(), // Paired token (B)_21 stableMode: false, // Use volatile pair pricing_21 uniqueID: nil_21)_21_21// 3. Execute Vault Source → Zapper workflow_21let availableTokens <- vaultSource.withdrawAvailable(maxAmount: 1000.0)_21let lpTokens <- zapper.swap(quote: nil, inVault: <-availableTokens)_21_21// Result: LP tokens ready for staking or further DeFi strategies_21log("LP tokens created: ".concat(lpTokens.balance.toString()))
Benefits:
- Capital Efficiency: Converts idle vault tokens to yield-generating LP positions
- Automated Balancing: Zapper handles optimal token split calculations automatically
- Single Transaction: Complex multi-step process executed atomically
- Minimum Protection: VaultSource ensures vault never goes below safety threshold
Price-Informed Rebalancing
Goal: Create autonomous rebalancing system based on price feeds
This sophisticated workflow creates an autonomous portfolio management system that maintains target value ratios by monitoring real-time price data. The AutoBalancer combines price oracles, sources, and sinks to automatically rebalance positions when they deviate from target thresholds.
How it works:
- Price oracle provides real-time asset valuations with staleness protection
- AutoBalancer tracks historical deposit values vs current market values
- When portfolio value exceeds upper threshold (120%), excess is moved to rebalance sink
- When portfolio value falls below lower threshold (80%), additional funds are sourced
- System maintains target allocation automatically without manual intervention
_19// Create autonomous rebalancing system_19let priceOracle = BandOracleConnectors.PriceOracle(_19 unitOfAccount: Type<@FlowToken.Vault>(),_19 staleThreshold: 3600, // 1 hour_19 feeSource: flowTokenSource,_19 uniqueID: nil_19)_19_19let autoBalancer <- FlowActions.createAutoBalancer(_19 vault: <-initialVault,_19 lowerThreshold: 0.8,_19 upperThreshold: 1.2,_19 source: rebalanceSource,_19 sink: rebalanceSink, _19 oracle: priceOracle,_19 uniqueID: nil_19)_19_19autoBalancer.rebalance(force: false) // Autonomous rebalancing
Benefits:
- Autonomous Operation: Maintains portfolio balance without manual intervention
- Risk Management: Prevents excessive exposure through automated position sizing
- Market Responsive: Adapts to price movements using real-time oracle data
- Threshold Flexibility: Configurable upper/lower bounds for different risk profiles
Restake & Compound Strategy
Goal: Automatically compound staking rewards back into the pool
This advanced compounding strategy maximizes yield by automatically claiming staking rewards and converting them back into LP tokens for re-staking. The workflow combines rewards claiming, zapping, and staking into a seamless compound operation that accelerates yield accumulation through reinvestment.
How it works:
- PoolRewardsSource claims accumulated staking rewards from the pool
- Zapper receives the reward tokens and converts them to LP tokens
- SwapSource orchestrates the rewards → LP token conversion process
- PoolSink re-stakes the new LP tokens back into the same pool
- Compound interest effect increases overall position size and future rewards
_31// Restake rewards workflow _31let rewardsSource = IncrementFiStakingConnectors.PoolRewardsSource(_31 poolID: 1, _31 staker: userAddress,_31 vaultType: Type<@FlowToken.Vault>(),_31 overflowSinks: {},_31 uniqueID: nil_31)_31_31let zapper = IncrementFiPoolLiquidityConnectors.Zapper(_31 token0Type: Type<@FlowToken.Vault>(),_31 token1Type: Type<@USDC.Vault>(),_31 stableMode: false,_31 uniqueID: nil_31)_31_31let swapSource = SwapConnectors.SwapSource(_31 swapper: zapper, _31 source: rewardsSource, _31 uniqueID: nil_31)_31_31let poolSink = IncrementFiStakingConnectors.PoolSink(_31 staker: userAddress, _31 poolID: 1,_31 uniqueID: nil_31)_31_31// Execute compound strategy_31let lpTokens <- swapSource.withdrawAvailable(maxAmount: UFix64.max)_31poolSink.depositCapacity(from: lpTokens)
Benefits:
- Compound Growth: Exponential yield increase through automatic reinvestment
- Gas Efficiency: Single transaction handles claim, convert, and re-stake operations
- Set-and-Forget: Automated compounding without manual intervention required
- Optimal Conversion: Zapper ensures efficient reward token to LP token conversion
Safety Best Practices
Always Check Capacity
Prevents transaction failures and enables graceful handling when sinks reach their maximum capacity limits. This is crucial for automated workflows that might encounter varying capacity conditions.
_10// Check before depositing_10if sink.depositCapacity(from: &vault) {_10 sink.depositCapacity(from: &vault)_10} else {_10 // Handle insufficient capacity_10}
Validate Balances
Ensures operations behave as expected and helps detect unexpected token loss or gain during complex workflows. Balance validation is essential for financial applications where token accuracy is critical.
_10// Verify operations completed successfully_10let beforeBalance = vault.balance_10sink.depositCapacity(from: &vault)_10let afterBalance = vault.balance_10_10assert(afterBalance >= beforeBalance, message: "Balance should not decrease")
Use Graceful Degradation
Prevents entire workflows from failing when individual components encounter issues. This approach enables robust strategies that can adapt to changing market conditions or temporary protocol unavailability.
_10// Handle failures gracefully_10if let result = try? operation.execute() {_10 // Success path_10} else {_10 // Fallback or no-op_10 log("Operation failed, continuing with strategy")_10}
Resource Management
Proper resource cleanup prevents token loss and ensures all vaults are properly handled, even when transactions partially fail. This is critical in Cadence where resources must be explicitly managed.
_10// Always clean up resources_10let vault = source.withdrawAvailable(amount)_10defer {_10 // Ensure vault is properly handled_10 if vault.balance > 0 {_10 // Return unused tokens_10 sourceVault.deposit(from: <-vault)_10 }_10}
Testing Your Combinations
Unit Testing
Tests individual connectors in isolation to verify they respect their constraints and behave correctly under various conditions. This catches bugs early and ensures each component works as designed.
_10// Test individual components_10test("VaultSource should maintain minimum balance") {_10 let source = VaultSource(min: 100.0, withdrawVault: vaultCap, uniqueID: nil)_10 _10 // Test minimum balance enforcement_10 let available = source.minimumAvailable()_10 assert(available >= 100.0, message: "Should maintain minimum balance")_10}
Integration Testing
Validates that multiple connectors work together correctly in complete workflows. This ensures the composition logic is sound and identifies issues that only appear when components interact.
_11// Test complete workflows_11test("Reward harvesting workflow should complete successfully") {_11 let workflow = RewardHarvestingWorkflow(_11 rewardsSource: rewardsSource,_11 swapper: swapper,_11 sink: sink_11 )_11 _11 let result = workflow.execute()_11 assert(result.success, message: "Workflow should complete successfully")_11}
Simulation Testing
Tests strategies under various market conditions using mock data to verify they respond appropriately to price changes, liquidity variations, and other market dynamics. This is essential for strategies that rely on external market data.
_16// Test with simulated market conditions_16test("Strategy should handle price volatility") {_16 let strategy = ArbitrageStrategy(_16 priceOracle: mockPriceOracle,_16 swapper: mockSwapper_16 )_16 _16 // Simulate price changes_16 mockPriceOracle.setPrice(1.0)_16 let result1 = strategy.execute()_16 _16 mockPriceOracle.setPrice(2.0)_16 let result2 = strategy.execute()_16 _16 assert(result1 != result2, message: "Strategy should adapt to price changes")_16}
📚 Next Steps
Now that you understand basic combinations, explore:
- Advanced Strategies: Complex multi-step workflows
- Risk Management: Advanced safety and monitoring techniques
- Custom Connectors: Building your own protocol adapters
Conclusion
In this tutorial, you learned how to combine Flow Actions primitives to create sophisticated workflows that leverage atomic composition, weak guarantees, and event traceability. You can now create and use Sources, Sinks, Swappers, Price Oracles, and Flashers, while utilizing UniqueIdentifiers to trace operations and compose complex atomic transactions.
Composability is the core strength of Flow Actions. These examples demonstrate how Flow Actions primitives can be combined to create powerful, automated workflows that integrate multiple protocols seamlessly. The framework's standardized interfaces enable developers to chain operations together like LEGO blocks, focusing on strategy implementation rather than protocol-specific integration details.