Cascading Dropdowns
Create dropdowns that update based on other field selections
Cascading dropdowns let users select from filtered options based on previous selections. For example: Department → Manager, or Country → State → City.
What You’ll Build
Section titled “What You’ll Build”A form where selecting a department dynamically populates a managers dropdown with only managers from that department.
Prerequisites
Section titled “Prerequisites”- Form with linked workflow
- Basic understanding of data providers
Create the Data Providers
Section titled “Create the Data Providers”Step 1: Create department provider (workspace/data_providers/departments.py):
from bifrost import data_provider, ExecutionContext
@data_provider( name="get_departments", description="List all departments", cache_ttl_seconds=3600 # Cache 1 hour)async def get_departments(context: ExecutionContext): return [ {"label": "Engineering", "value": "eng"}, {"label": "Sales", "value": "sales"}, {"label": "Support", "value": "support"} ]Step 2: Create managers provider with department parameter:
from bifrost import data_provider, param, ExecutionContext
@data_provider(name="get_managers_by_dept")@param("department", type="string", required=True)async def get_managers_by_dept(context: ExecutionContext, department: str): # Fetch managers filtered by department managers = await db.query( "SELECT id, name FROM users WHERE department = ? AND is_manager = true", department )
return [ {"label": mgr["name"], "value": mgr["id"]} for mgr in managers ]Configure the Workflow
Section titled “Configure the Workflow”Add both parameters to your workflow:
from bifrost import workflow, param, ExecutionContext
@workflow(name="assign_to_manager")@param("department", type="string", data_provider="get_departments")@param("manager", type="string", data_provider="get_managers_by_dept")async def assign_to_manager( context: ExecutionContext, department: str, manager: str): # Assign work to selected manager return { "department": department, "manager_id": manager, "assigned": True }Configure Form Fields
Section titled “Configure Form Fields”-
In form builder, drag the department field to canvas
- Auto-configured with
get_departmentsprovider
- Auto-configured with
-
Drag the manager field to canvas
- Click to edit field
- Under Data Provider, select
get_managers_by_dept
-
Configure Data Provider Inputs:
- Click Data Provider Inputs
- For
departmentparameter:- Mode: Field Reference
- Field Name:
department
Test It
Section titled “Test It”- Save the form
- Click Launch
- Select a department
- Watch the managers dropdown populate with filtered results
- Select a manager and submit
Advanced Patterns
Section titled “Advanced Patterns”Three-Level Cascade (Country → State → City)
Section titled “Three-Level Cascade (Country → State → City)”@data_provider(name="get_countries")async def get_countries(context): return [ {"label": "United States", "value": "us"}, {"label": "Canada", "value": "ca"} ]
@data_provider(name="get_states")@param("country", type="string", required=True)async def get_states(context, country: str): states = await db.query( "SELECT * FROM states WHERE country_code = ?", country ) return [{"label": s.name, "value": s.code} for s in states]
@data_provider(name="get_cities")@param("state", type="string", required=True)async def get_cities(context, state: str): cities = await db.query( "SELECT * FROM cities WHERE state_code = ?", state ) return [{"label": c.name, "value": c.id} for c in cities]Configure in workflow:
@param("country", type="string", data_provider="get_countries")@param("state", type="string", data_provider="get_states")@param("city", type="string", data_provider="get_cities")In form, configure Data Provider Inputs:
- state field →
countryparameter → Field Reference:country - city field →
stateparameter → Field Reference:state
Multiple Parameters
Section titled “Multiple Parameters”Pass multiple field values to a provider:
@data_provider(name="get_filtered_products")@param("category", type="string", required=True)@param("price_range", type="string", required=True)async def get_filtered_products(context, category: str, price_range: str): products = await db.query( "SELECT * FROM products WHERE category = ? AND price_range = ?", category, price_range ) return [{"label": p.name, "value": p.id} for p in products]In form Data Provider Inputs:
categoryparameter → Field Reference:categoryprice_rangeparameter → Field Reference:price_range
Static + Field Reference
Section titled “Static + Field Reference”Mix static values with field references:
@data_provider(name="get_regional_managers")@param("department", type="string", required=True)@param("region", type="string", required=True)async def get_regional_managers(context, department: str, region: str): # region comes from static value, department from form field return await fetch_managers(department, region)In form Data Provider Inputs:
departmentparameter → Field Reference:departmentregionparameter → Static:"west_coast"
Expression Mode
Section titled “Expression Mode”Use JavaScript expressions for computed values:
In form Data Provider Inputs:
- Mode: Expression
- Value:
context.field.country + '_' + context.field.state
Common Patterns
Section titled “Common Patterns”Organization-Scoped Cascading
Section titled “Organization-Scoped Cascading”Filter all levels by organization:
@data_provider(name="get_org_departments")async def get_org_departments(context): return await db.query( "SELECT * FROM departments WHERE org_id = ?", context.org_id )
@data_provider(name="get_org_managers")@param("department", type="string", required=True)async def get_org_managers(context, department: str): return await db.query( "SELECT * FROM users WHERE org_id = ? AND department = ? AND is_manager = true", context.org_id, department )Empty State Handling
Section titled “Empty State Handling”Show helpful message when no options available:
@data_provider(name="get_managers_by_dept")@param("department", type="string", required=True)async def get_managers_by_dept(context, department: str): managers = await db.query( "SELECT * FROM users WHERE department = ? AND is_manager = true", department )
if not managers: return [{"label": "No managers in this department", "value": "", "disabled": True}]
return [{"label": m.name, "value": m.id} for m in managers]Troubleshooting
Section titled “Troubleshooting”Dropdown not updating: Verify Data Provider Inputs configuration. Field Name must exactly match the source field name.
Empty dropdown: Check provider returns correct format with label and value. Test provider independently.
Slow updates: Increase cache TTL on parent provider. Use shorter TTL on dependent provider for freshness.
Best Practices
Section titled “Best Practices”- Cache parent providers: Long TTL on rarely-changing data (departments, countries)
- Short cache on dependent providers: Balance freshness with performance
- Limit results: Return max ~100 options per dropdown
- Show loading states: Dependent providers may take time to fetch
- Test edge cases: What if parent selection is cleared?
Next Steps
Section titled “Next Steps”- Visibility Rules - Show/hide fields conditionally
- HTML Content - Display dynamic content