Execution Engine
The Execution Engine, located within the @execution core package, is responsible for running automations defined in the application's database. It orchestrates the flow of data and control between different steps of an automation, manages execution state, and interacts with various services like logging and data persistence.
Overview
At its heart, the Execution Engine processes a series of "steps" linked by "connections." Each automation run begins with a designated start step. The engine executes this step, then uses its output and the logic defined in its outgoing connections to determine the next step. This process repeats until a terminal step is reached or no further valid connections are found.
The primary components involved in this process are:
ExecutionEngine: The main orchestrator.ExecutionContext: Manages the state and context of a single automation run.StepExecutorFactory: Creates specific executors for different types of steps.BaseStepExecutor: Provides common functionality for all step executors.- Concrete Step Executors: Implement the specific logic for each step type (e.g.,
LLMStepExecutor,ActionStepExecutor). ConnectionEvaluator: Determines the next step in the workflow.
Core Components
1. ExecutionEngine.ts
The ExecutionEngine class is the central orchestrator for automation executions.
- Initiation: It begins an automation run by creating an
execution_runsrecord in the database (Supabase). - Data Loading: It fetches the automation's definition, including all its steps and the connections between them, from the database.
- Context Creation: It initialises an
ExecutionContextobject to manage the state for this specific run. - Workflow Execution: It calls the
executeWorkflowmethod, passing the starting step ID, all steps and connections, initial inputs, and theExecutionContext. - Recursive Execution: The
executeWorkflowmethod:- Identifies the current step to be executed.
- Uses the
StepExecutorFactoryto create an appropriate executor for this step. - The step executor processes the step.
- The
ConnectionEvaluatordetermines the next step based on the current step's output and connection logic. - The engine then recursively calls
executeWorkflowfor the next step.
- Completion/Error Handling: Upon successful completion or if an error occurs, the
ExecutionEngineupdates theexecution_runsrecord in the database with the final status, result, or error details.
2. ExecutionContext.ts
The ExecutionContext class acts as a stateful container for a single automation run. Its key responsibilities include:
- Run Identifiers: Stores
runId,automationId, anduserId. - Variable Management: Maintains a central store for all variables created and modified during the execution. This includes initial inputs to the automation and outputs from each step. Variables are accessible via a path-based system (e.g.,
steps.step_id.outputs.some_key). - Step Results: Keeps a record of the output from each executed step.
- Execution Path: Tracks the sequence of steps executed.
- Persistence:
- Manages an
ExecutionDocumentwhich is a snapshot of the entire execution state, saved periodically or upon completion. - Saves individual step execution details (inputs, outputs, status, start/end times) to the
step_executionstable in Supabase.
- Manages an
- Service Provision:
ExecutionLogger: Provides logging capabilities, recording events like step starts, completions, errors, and transitions between steps.ExecutionAssetsService: Manages any assets (e.g., files) generated or used during the execution.ExecutionEventService: Handles the dispatch of events related to the execution lifecycle.
- Iteration State: Manages the state for iterative loops, including the current item, index, and accumulated results, ensuring this state is available across steps involved in a loop.
3. StepExecutorFactory.ts
This factory class is responsible for instantiating the correct step executor based on a step's type (e.g., 'llm', 'action', 'trigger').
- It maintains a map of registered step types to their corresponding executor classes.
- When the
ExecutionEngineneeds to execute a step, it callsStepExecutorFactory.createExecutor(step, supabaseClient), which returns an instance of the appropriate executor (e.g.,LLMStepExecutor).
4. BaseStepExecutor.ts
This abstract class provides a common structure and shared logic for all specific step executors.
- Constructor: Takes the
Stepobject and aSupabaseClientinstance. executeStepMethod: This is the main public method called by theExecutionEngine. It orchestrates:- Input Preparation: Calls
prepareInputsFromSchemato resolve any variable placeholders in the incoming data. The step's configuration contains aninputSchemawhich defines how input fields should be populated, often referencing outputs from previous steps or global variables stored in theExecutionContext. For example, an input field might be defined to take its value from\{\{variables.initial_query\}\}or\{\{steps.previous_step_id.outputs.summary\}\}. It also correctly populates iteration-specific data if the step is part of a loop. - Recording Inputs: Records the resolved inputs with the
ExecutionContext. - Logging: Logs the start of the step execution via the
ExecutionLogger. - Calling Abstract
execute: Invokes theexecutemethod, which must be implemented by concrete subclasses. - Result/Error Handling: Calls
completeExecutionorhandleExecutionErrorto save the outcome via theExecutionContext.
- Input Preparation: Calls
executeMethod (Abstract): Concrete step executors (e.g.,LLMStepExecutor,ActionStepExecutor) must implement this method to perform their specific tasks, such as making an API call, processing data, or interacting with an external service.- Input Schema Handling: The
getInputSchemamethod retrieves the input schema defined within the step's configuration. This schema details the expected inputs, their types, default values, and crucially, how dynamic values (variables) should be resolved from theExecutionContext.
5. Concrete Step Executors (e.g., LLMStepExecutor.ts, ActionStepExecutor.ts)
These classes extend BaseStepExecutor and implement the execute method to define the specific logic for a particular type of step. For instance:
TriggerStepExecutor: Handles the initiation of an automation based on a trigger condition.LLMStepExecutor: Interacts with a Large Language Model, sending prompts and receiving responses.ActionStepExecutor: Executes a predefined action, which might involve custom code or interaction with third-party services via adapters.
Each executor uses the resolved inputs provided by BaseStepExecutor and, after performing its task, returns an StepOutput object.
6. ConnectionEvaluator.ts
The ConnectionEvaluator class is pivotal in determining the flow of execution after a step completes.
- Input: Receives the list of outgoing connections from the completed step, the step's output, and the current
ExecutionContext. - Evaluation Logic:
- It iterates through the connections, prioritising specific
branch_typeconnections over thedefaultconnection. condition: If a connection has abranch_typeofcondition, itscondition_expression(e.g.,inputs.status === 'completed' && \{\{variables.customer_score\}\} > 50) is evaluated.- Variable placeholders (like
\{\{variables.customer_score\}\}) within the expression are first resolved using values from theExecutionContext. - The resolved expression is then dynamically converted into a JavaScript function and executed. If it returns
true, this connection is chosen.
- Variable placeholders (like
iteration: For loops, this branch type initiates an iteration.initializeIteration: It identifies the array of items to iterate over usingconnection.iteration_items_path(which points to a path in the previous step's output, e.g.,outputs.data.records) and the name for the current item during iteration (connection.iteration_item_name). The initial iteration state (items, current index, etc.) is stored in theExecutionContext. The first item and iteration details are passed as state to the next step.
iteration_continue: This branch type is used for subsequent iterations in a loop.advanceIteration: It retrieves the current iteration state fromExecutionContext, advances to the next item, collects the result from the previous iteration step, and updates the iteration state. If more items remain, this connection is chosen, and the new current item and updated iteration state are passed to the next step.
iteration_complete: If an iteration finishes (no more items), a connection with this branch type (if present) is chosen.default: If no conditional or iteration branch evaluates to true or is applicable, a connection marked asdefault(or the one without abranch_type) is chosen.
- It iterates through the connections, prioritising specific
- Output: Returns the selected
StepConnectionand, crucially for iterations, an updatedstateobject. Thisstate(which includes the current iteration item and context) will form the input for the next step in the workflow.
Adapters and Capability Execution
A crucial aspect of the Execution Engine's architecture is its ability to interact with various third-party services and internal capabilities. This is facilitated by an adapter-based system.
1. BaseAdapter Interface (types.ts)
The foundation of this system is the BaseAdapter interface, which all specific adapters must implement. Key methods include:
getAdapterInfo(): Returns metadata about the adapter, including:provider: A string identifying the service (e.g., "slack", "openai", "youtube").capabilities: An array of strings listing the specific actions the adapter can perform (e.g., "send_message", "generate_text", "upload_video").description: A human-readable description of the adapter.
getCapabilities(): Directly returns an array of capability names supported by the adapter.executeCapability<T>(capabilityName: string, inputs: unknown, config: unknown): Promise<T>: This is the primary method for performing an action.capabilityName: The specific capability to execute (e.g., "send_message").inputs: The data required by the capability, typically resolved by theBaseStepExecutorfrom the step's configuration and theExecutionContext.config: Any provider-specific configuration needed for the capability (e.g., API keys, authentication tokens). This configuration would be securely managed and made available to the adapter.
2. Concrete Adapters
Specific adapters are created for each third-party provider or distinct set of functionalities. For example:
SlackAdapter: Might implement capabilities like "send_message", "create_channel", "lookup_user".OpenAIAdapter: Might implement "generate_text", "create_embedding".YouTubeAdapter: Might implement "search_videos", "get_video_details".
Each concrete adapter implements the executeCapability method by translating the generic request into the specific API calls and data transformations required by its target provider.
3. Adapter Registry (Registry.ts) and Discovery Service (DiscoveryService.ts)
The system utilises a structured approach for managing and discovering adapters and their capabilities, primarily through AdapterRegistry.ts and DiscoveryService.ts.
AdapterRegistry.ts
- Generic Registry Management: This file defines a generic
AdapterRegistry<T extends BaseAdapter>class. Instead of a single global registry, this class acts as a blueprint for creating multiple, type-specific registries. - Specific Registry Instances: The system creates distinct registry instances for different categories of operations. As seen in
Registry.ts, these include:LLMRegistry: Manages adapters related to Large Language Models (implementingBaseLLMAdapter).ActionRegistry: Manages adapters for general actions (implementingBaseActionAdapter).TriggerRegistry: Manages adapters for triggering automations (implementingBaseTriggerAdapter).
- Adapter Registration: Adapters are registered with their respective registry (e.g., a new LLM adapter would be registered with
LLMRegistry). TheregisterAdaptermethod is crucial here, as it stores:- The adapter class itself.
- The
providerstring (e.g., "openai", "slack"). - A descriptive string for the provider.
- A
Record<string, CapabilityMetadata>, which maps eachcapabilityNamethe adapter offers (e.g., "generate_text") to itsCapabilityMetadata. This metadata includes theinputSchema,outputSchema, and an optionalconfigSchemafor that specific capability.
- Adapter Instantiation: To execute a capability, a step executor needs an instance of the relevant adapter. It would use the
getAdapter(provider, supabase, context)method of the appropriate registry (e.g.,ActionRegistry.getAdapter(...)). This method instantiates the adapter class, injecting the necessarySupabaseClientandExecutionContextdependencies.
DiscoveryService.ts
- Unified Discovery Layer: This static class provides a higher-level API to query and discover available functionalities across all the different registries (
LLMRegistry,ActionRegistry,TriggerRegistry). - Key Discovery Methods:
getAllProviders(): Returns a comprehensive list of all registered providers, grouped by their type (llm, action, trigger), along with descriptions and capability counts. Useful for UI elements that allow users to browse available integrations.getCapabilities(type, provider): Fetches the fullCapabilityMetadatafor all capabilities offered by a specificproviderwithin a giventype(e.g., all capabilities for the "slack" provider in theActionRegistry).getProviderCapabilityConfig(type, provider, capability): Allows querying for the detailed configuration of a single, specific capability. This includes itsinputSchema(what data it needs),outputSchema(what data it produces), and anyconfigSchema(provider-level configuration parameters that might be relevant for this capability). This is vital for dynamically rendering configuration forms for steps and for validation.getProviderCapabilities(type, provider): Returns a simple list of capability names for a given provider and type.
This two-tiered approach (specific registries for management, and a global discovery service for querying) provides both organized management of adapters and an easy way for the system (and potentially UIs) to understand what capabilities are available and how to use them.
4. Role in Step Execution
Adapters are integral to how steps, particularly action and llm steps, function:
- Step Definition: When defining an automation, a step that interacts with an external service (e.g., "Send Slack Message" or "Translate Text with OpenAI") will have its
StepConfigspecify:provider: The name of the provider (e.g., "slack", "openai").action(or a similar field likemodelfor LLMs): This corresponds to acapabilityNamesupported by the specified provider's adapter (e.g., "send_message", "gpt-3.5-turbo").inputSchema: Defines the inputs expected by this specific capability.
- Execution Time:
- The
ExecutionEngineprocesses the step. - The
StepExecutorFactorycreates the appropriate executor (e.g.,ActionStepExecutor). - The
BaseStepExecutorprepares the inputs according to the step'sinputSchemaand the currentExecutionContext. - The concrete step executor (e.g.,
ActionStepExecutor): a. Uses theproviderandaction(capability) names from itsStepConfig. b. Requests the corresponding adapter instance from the appropriate registry (e.g.,ActionRegistry.getAdapter("slack", supabaseClient, context)). c. Callsadapter.executeCapability(actionName, resolvedInputs, adapterConfig). - The adapter performs the interaction with the third-party service.
- The result is returned to the step executor, which then passes it to the
ExecutionContextto be saved and made available to subsequent steps.
- The
This decoupled architecture allows for:
- Extensibility: New providers and capabilities can be added by creating new adapters and registering them, without modifying the core
ExecutionEnginelogic. - Modularity: Logic for interacting with specific external services is encapsulated within their respective adapters.
- Consistency: Step executors can interact with various providers through a uniform
executeCapabilityinterface.
The CapabilityMetadata (retrieved via the DiscoveryService and stored by the AdapterRegistry) for each capability, with its defined inputSchema and outputSchema, is critical. It allows the BaseStepExecutor to correctly prepare inputs using prepareInputsFromSchema and enables the ConnectionEvaluator to effectively use the step's outputs for conditional logic and routing to subsequent steps.
Execution Flow Summary
- An automation execution is triggered (e.g., via API call or a scheduled event).
- The
ExecutionEnginecreates anexecution_runsrecord and anExecutionContext. - It fetches the automation's start step ID, all associated steps, and connections from the database.
- The
executeWorkflowmethod is called with the start step. - Inside
executeWorkflow: a. The current step is identified. b.StepExecutorFactorycreates the relevant step executor (e.g.,LLMStepExecutor). c. TheBaseStepExecutor'sexecuteStepmethod: i. Prepares inputs using the step'sinputSchemaandExecutionContext(resolving variables, setting up iteration items). ii. Records inputs and logs the step start. iii. Calls the concrete executor'sexecutemethod (e.g.,LLMStepExecutor.execute). iv. The concrete executor performs its task and returns an output. v. Saves the output or error toExecutionContextand updates thestep_executionsrecord. d.ConnectionEvaluator.evaluateConnectionsis called with the completed step's outgoing connections, its output, and theExecutionContext. e. The evaluator selects the next connection (and provides iteration state if applicable) based on conditions or loop progression. f. If a next step is found,executeWorkflowis called recursively for that step, using the output (or iteration state) from the evaluator as input. g. If no next step is found (e.g., end of a branch, or a terminal step), the recursion unwinds. - Once the workflow is complete (or fails), the
ExecutionEngineupdates the mainexecution_runsrecord with the final status and result.
This architecture allows for complex workflows with conditional logic, loops, and various types of operations, all while maintaining a clear record of the execution process and state.