Agent skill
finwiz-flow-architecture
Critical Flow architecture lessons for FinWiz including business logic flow, state management, and crew integration patterns. Use when designing or debugging CrewAI Flows.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/finwiz-flow-architecture
SKILL.md
FinWiz Flow Architecture Lessons
Critical lessons from deep portfolio analysis implementation and flow sequence corrections.
Lesson 1: Flow Sequence Must Match Business Logic
The Problem
The original flow had discovery crews running BEFORE portfolio analysis. This is backwards from the logical business process.
Correct Business Flow
- Analyze what you have - Portfolio holdings analysis
- Find alternatives - Replace underperforming holdings
- Discover new opportunities - Find additional A+ investments
- Propose rebalancing - Optimize allocation
- Present report - Consolidated recommendations
Implementation Rule
ALWAYS design flow sequences to match the logical business process, not technical convenience.
# ✅ CORRECT: Portfolio analysis before discovery
@start()
def validate_data_integration(self):
pass
@listen("validate_data_integration")
def check_portfolio(self): # Portfolio analysis FIRST
pass
@listen("analyze_and_update_portfolio")
def check_crypto(self): # Discovery AFTER
pass
# ❌ WRONG: Discovery before portfolio analysis
@listen("validate_data_integration")
def check_crypto(self): # Discovery crew
pass
@listen(and_("check_crypto", "check_stock", "check_etf"))
def check_portfolio(self): # Portfolio analysis
pass
Lesson 2: Consolidate Related Operations
The Problem
Three separate Flow methods performed related operations sequentially:
analyze_holdings_deep()- Run crewsmatch_alternatives()- Find alternativesupdate_portfolio_review_with_deep_analysis()- Regenerate portfolio
This caused:
- Portfolio review generated TWICE (inefficient)
- Race conditions (discovery could start before portfolio update)
- Complex dependency chains (3 @listen decorators)
Solution
Consolidate into ONE atomic operation:
@listen("check_portfolio")
def analyze_and_update_portfolio(self) -> dict[str, Any]:
"""Atomic operation: deep analysis + alternatives + portfolio update."""
# Step 1: Deep analysis
deep_results = self._run_deep_analysis_on_holdings()
# Step 2: Match alternatives
alternatives = self._match_alternatives_for_holdings(deep_results)
# Step 3: Update portfolio (ONCE)
portfolio_updated = self._update_portfolio_review_with_enriched_data()
return consolidated_results
Implementation Rule
When operations are logically related and sequential, consolidate them into a single atomic method with helper functions.
Benefits:
- ✅ Single portfolio generation (not twice)
- ✅ No race conditions
- ✅ Simpler dependency chain
- ✅ Atomic semantics (all-or-nothing)
Lesson 3: Fix Listener Dependencies Carefully
The Problem
@listen(and_("match_alternatives", "check_portfolio_rebalancing"))
def check_investment_discovery(self):
pass
This listener waited for match_alternatives but NOT for update_portfolio_review_with_deep_analysis, which also listened to match_alternatives. This created a race condition.
Solution
After consolidation, the listener waits for the complete atomic operation:
@listen(and_("analyze_and_update_portfolio", "check_portfolio_rebalancing"))
def check_investment_discovery(self):
pass
Implementation Rule
When using @listen decorators, ensure dependencies wait for COMPLETE operations, not intermediate steps.
Lesson 4: Discovery vs Deep Analysis Separation
The Problem
Discovery crews (StockCrew, EtfCrew, CryptoCrew) were being misused for single-ticker deep analysis, causing:
- 3-6 hour hangs
- Reasoning agents asking for "10 tickers"
- Infinite loops with
'ready': False
Solution
Create separate crews with clear purposes:
Discovery Crews (existing):
- Purpose: Screen and find "top 10" assets
- Input: No specific tickers
- Output: List of opportunities
- Use case: Investment discovery
Deep Analysis Crew (new):
- Purpose: Analyze ONE specific ticker
- Input: ticker + asset_class
- Output: DeepAnalysisResult with grade
- Use case: Portfolio holdings evaluation
Implementation Rule
Create separate crews for different use cases. Don't force a "top 10" crew to analyze single tickers.
Lesson 5: Dynamic Tool Routing Eliminates Duplication
The Problem Avoided
Could have created 3 separate deep analysis crews (StockDeepAnalysisCrew, EtfDeepAnalysisCrew, CryptoDeepAnalysisCrew), leading to code duplication.
Solution
ONE unified crew with dynamic tool routing:
def get_tools_for_asset_class(self, asset_class: str) -> list:
"""Route to appropriate tool set based on asset class."""
if asset_class.lower() == "stock":
return get_stock_crew_tools(...)
elif asset_class.lower() == "etf":
return get_etf_crew_tools(...)
elif asset_class.lower() == "crypto":
return get_crypto_crew_tools(...)
else:
raise ValueError(f"Invalid asset_class: {asset_class}")
Implementation Rule
When crews differ only in tool selection, use dynamic routing instead of creating separate crew classes.
Lesson 6: Test What You Control, Not AI Behavior
The Problem Avoided
Initial test plans included testing agent behavior, LLM calls, and crew execution results.
Solution
Focus tests on:
- ✅ Tool routing logic
- ✅ Configuration loading
- ✅ Helper methods with mocks
- ✅ Flow state management
- ✅ Error handling
- ✅ Data parsing
Do NOT test:
- ❌ Agent behavior
- ❌ LLM calls
- ❌ Crew execution
- ❌ Reasoning loops
Implementation Rule
Only test deterministic logic you control. Mock all AI/LLM behavior.
Lesson 7: Reasoning-Compatible Task Descriptions
The Problem
Task descriptions with "top 10" language caused reasoning agents to request multiple tickers when only one was provided.
Solution
Explicit single-ticker task descriptions:
deep_analysis_task:
description: >
Perform comprehensive analysis of the provided {asset_class} ticker: {ticker}
SINGLE TICKER MODE: You are analyzing ONE specific {asset_class}, not screening multiple assets.
The ticker {ticker} is provided as input. Do NOT request additional tickers.
Analysis Steps for {asset_class}:
1. Validate {ticker} using TickerValidationTool
2. Fetch {asset_class}-specific data for {ticker}
...
Implementation Rule
When using reasoning=True, task descriptions must explicitly state the mode (single ticker vs multiple) and repeat the input variable throughout.
Key phrases:
- "SINGLE TICKER MODE"
- "the provided ticker: {ticker}"
- "Do NOT request additional tickers"
- Repeat "{ticker}" throughout description
Lesson 8: Final Reporter Must Have Empty Tools
The Problem Avoided
Final reporters should consolidate from context, not make external API calls.
Solution
Use @final_reporter decorator to enforce empty tools:
@final_reporter
@agent
def investment_reporter(self) -> Agent:
return Agent(
config=self.agents_config["investment_reporter"],
tools=[], # MUST be empty - enforced by decorator
verbose=True
)
Implementation Rule
Final reporters in crews must have empty tools lists. Use @final_reporter decorator to enforce this at framework level.
Lesson 9: JSON Serialization of Datetime Objects
The Problem
Tools returning JSON strings with json.dumps() failed when data contained datetime objects:
ERROR - Error in technical analysis: Object of type datetime is not JSON serializable
Solution
Always use default=str parameter with json.dumps():
# ❌ WRONG - Will fail with datetime objects
tech_data = {"timestamp": datetime.now(), "value": 123.45}
return json.dumps(tech_data, indent=2)
# ✅ CORRECT - Handles datetime and other non-serializable types
tech_data = {"timestamp": datetime.now(), "value": 123.45}
return json.dumps(tech_data, indent=2, default=str)
Alternative: Use Pydantic Serialization
# ✅ BEST - Pydantic handles datetime serialization automatically
result = QuantitativeBacktestResult(
backtest_start_date=datetime.now(),
backtest_end_date=datetime.now(),
# ... other fields
)
return result.model_dump_json(indent=2)
Implementation Rule
When manually serializing data with json.dumps(), always include default=str to handle datetime, numpy types, and other non-JSON-serializable objects. Prefer Pydantic's model_dump_json() when working with models.
Summary Checklist
When designing CrewAI Flows:
- Flow sequence matches logical business process
- Related operations consolidated into atomic methods
- Listener dependencies wait for complete operations
- Discovery and deep analysis crews are separate
- Dynamic tool routing used instead of duplicate crews
- Tests focus on logic, not AI behavior
- Task descriptions are reasoning-compatible
- Final reporters have empty tools (enforced by decorator)
- Portfolio/data generated once, not multiple times
- No race conditions in parallel listeners
- JSON serialization uses
default=stror Pydantic'smodel_dump_json()
Apply these architectural patterns to avoid common Flow design pitfalls and ensure robust, maintainable CrewAI implementations.
Didn't find tool you were looking for?