Nova Act supports running multiple browser sessions in parallel. This enables powerful patterns like map-reduce for web automation, where you can distribute tasks across multiple browsers and aggregate results.
One NovaAct instance can only actuate one browser at a time. However, multiple NovaAct instances can run concurrently as they are lightweight.
The recommended approach for parallel sessions is using Python’s ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor, as_completedfrom nova_act import NovaActdef process_item(item): """Process a single item in its own browser session.""" with NovaAct( starting_page="https://example.com", headless=True, # Run headless for parallel sessions ) as nova: result = nova.act_get(f"Search for {item} and return the price") return result.responseitems = ["laptop", "mouse", "keyboard"]with ThreadPoolExecutor() as executor: future_to_item = { executor.submit(process_item, item): item for item in items } results = {} for future in as_completed(future_to_item.keys()): item = future_to_item[future] try: result = future.result() results[item] = result print(f"✓ {item}: {result}") except Exception as e: print(f"✗ {item} failed: {e}")
Real-World Example: Apartment Search with Commute Calculation
This example searches for apartments and calculates commute times in parallel:
from concurrent.futures import ThreadPoolExecutor, as_completedfrom typing import Literalimport pandas as pdfrom pydantic import BaseModelfrom nova_act import NovaActclass Apartment(BaseModel): address: str price: str beds: str baths: strclass ApartmentList(BaseModel): apartments: list[Apartment]class TransitCommute(BaseModel): commute_time_hours: int commute_time_minutes: int commute_distance_miles: floatdef add_commute_distance( apartment: Apartment, transit_city: str, transport_mode: Literal["walking", "biking"], maps_url: str,) -> TransitCommute | None: """Calculate commute time for a single apartment.""" with NovaAct( starting_page=maps_url, headless=True, # Run in headless mode for parallel execution ) as nova: result = nova.act_get( f"Search for {transit_city} transit station and press enter. " "Click Directions. " f"Enter '{apartment.address}' into the starting point field and press enter. " f"Return the shortest {transport_mode} time and distance.", schema=TransitCommute.model_json_schema(), ) return TransitCommute.model_validate(result.parsed_response)def main(): apartment_url = "https://example-apartments.com" maps_url = "https://example-maps.com" transit_city = "Downtown Station" transport_mode = "walking" # Step 1: Search for apartments (synchronous, in headed mode) all_apartments = [] with NovaAct( starting_page=apartment_url, headless=False, # Show browser for initial search ) as nova: nova.act( "Close any cookie banners. " f"Search for apartments near {transit_city}, " "then filter for 2 bedrooms and 1 bathroom." ) result = nova.act_get( "Return the currently visible list of apartments", schema=ApartmentList.model_json_schema(), ) apartment_list = ApartmentList.model_validate(result.parsed_response) all_apartments.extend(apartment_list.apartments) print(f"✓ Found {len(all_apartments)} apartments") # Step 2: Calculate commute times in parallel (asynchronous, headless) apartments_with_commute = [] with ThreadPoolExecutor() as executor: future_to_apartment = { executor.submit( add_commute_distance, apartment, transit_city, transport_mode, maps_url, ): apartment for apartment in all_apartments } for future in as_completed(future_to_apartment.keys()): apartment = future_to_apartment[future] try: commute_details = future.result() if commute_details: apartments_with_commute.append( apartment.model_dump() | commute_details.model_dump() ) except Exception as e: print(f"Failed to get commute for {apartment.address}: {e}") apartments_with_commute.append(apartment.model_dump()) # Step 3: Display results sorted by commute time apartments_df = pd.DataFrame(apartments_with_commute) sorted_apartments = apartments_df.sort_values( by=["commute_time_hours", "commute_time_minutes", "commute_distance_miles"] ) print(f"\n✓ Apartments sorted by {transport_mode} time:") print(f"\n{sorted_apartments.to_string()}\n")if __name__ == "__main__": main()
# Clone the shared directory for each instanceshared_user_data_dir = "/path/to/shared/profile"with NovaAct( starting_page="https://example.com", user_data_dir=shared_user_data_dir, clone_user_data_dir=True, # Each instance gets its own copy) as nova: nova.act("Do something")
If running multiple NovaAct instances in parallel with a shared user_data_dir, you MUST set clone_user_data_dir=True. Otherwise, browser instances will conflict.
When using the Workflow context manager with threads:
from threading import Threadfrom nova_act import NovaAct, Workflowdef multi_threaded_helper(workflow: Workflow): """Helper function that runs in a separate thread.""" with NovaAct( starting_page="https://example.com", workflow=workflow, # Pass workflow explicitly ) as nova: nova.act("Process data")with Workflow( workflow_definition_name="my-workflow", model_id="nova-act-latest") as workflow: # Start thread with workflow context t = Thread(target=multi_threaded_helper, args=(workflow,)) t.start() t.join()
When using the @workflow decorator, you need to copy the context:
from contextvars import copy_contextfrom threading import Threadfrom nova_act import NovaAct, workflowdef multi_threaded_helper(): """Helper function that runs in a separate thread.""" with NovaAct(starting_page="https://example.com") as nova: nova.act("Process data")@workflow( workflow_definition_name="my-workflow", model_id="nova-act-latest",)def multi_threaded_workflow(): # Copy context for thread ctx = copy_context() t = Thread(target=ctx.run, args=(multi_threaded_helper,)) t.start() t.join()multi_threaded_workflow()
Alternatively, use get_current_workflow() to manually inject context:
from threading import Threadfrom nova_act import NovaAct, get_current_workflow, workflowdef multi_threaded_helper(workflow_obj): with NovaAct( starting_page="https://example.com", workflow=workflow_obj, ) as nova: nova.act("Process data")@workflow( workflow_definition_name="my-workflow", model_id="nova-act-latest",)def multi_threaded_workflow(): # Get current workflow and pass to thread t = Thread( target=multi_threaded_helper, args=(get_current_workflow(),) ) t.start() t.join()multi_threaded_workflow()
Run parallel sessions in headless mode to reduce resource usage:
with NovaAct(starting_page="...", headless=True) as nova: # Process in background
Handle Errors Gracefully
Always use try-except blocks when processing parallel results:
for future in as_completed(futures): try: result = future.result() process_result(result) except Exception as e: print(f"Task failed: {e}") # Handle failure appropriately
Limit Concurrency
Don’t overwhelm your system or target websites:
# Limit to 5 concurrent sessionswith ThreadPoolExecutor(max_workers=5) as executor: # Submit tasks
Monitor Resource Usage
Each browser instance consumes CPU and memory. Monitor system resources and adjust concurrency accordingly.
Multi-processing is not currently supported. The Workflow construct maintains a boto3 Session and Client as instance variables, which are not pickle-able. Use threading instead.