Composing Workflows with Flow Actions
Flow Actions are designed to be composable, which means you can chain them together like LEGO blocks to build complex strategies. Each primitive has a standardized interface that works consistently across all protocols and eliminates the need to learn multiple APIs. This composability allows atomic execution of multi-step workflows within single transactions, ensuring either complete success or safe failure. When developers combine these primitives, they create sophisticated decentralized finance (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 rather than reverting when deposits exceed capacity, which allows 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 you complete 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
- 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 (for example, FLOW) to stable tokens (for example, 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 (for example, 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 allows 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 with 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 monitors real-time price data to maintain target value ratios. 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 with 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 allows 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 allows 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 you must explicitly manage resources.
_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.