syntax = "proto3"; package aggregator; option go_package = "./avsproto"; import "google/protobuf/struct.proto"; // ============================================================================ // ENUMS - Single source of truth for types // ============================================================================ // TriggerType defines all supported trigger types // This enum provides a single source of truth for trigger types used across the system enum TriggerType { TRIGGER_TYPE_UNSPECIFIED = 0; // Default unspecified value TRIGGER_TYPE_MANUAL = 1; // Manual trigger TRIGGER_TYPE_FIXED_TIME = 2; // Fixed time trigger TRIGGER_TYPE_CRON = 3; // Cron-based trigger TRIGGER_TYPE_BLOCK = 4; // Block-based trigger TRIGGER_TYPE_EVENT = 5; // Event-based trigger } // NodeType defines all supported node types // This enum provides a single source of truth for node types used across the system enum NodeType { NODE_TYPE_UNSPECIFIED = 0; // Default unspecified value NODE_TYPE_ETH_TRANSFER = 1; // Ethereum transfer node NODE_TYPE_CONTRACT_WRITE = 2; // Contract write node NODE_TYPE_CONTRACT_READ = 3; // Contract read node NODE_TYPE_GRAPHQL_QUERY = 4; // GraphQL query node NODE_TYPE_REST_API = 5; // REST API node NODE_TYPE_CUSTOM_CODE = 6; // Custom code node NODE_TYPE_BRANCH = 7; // Branch node NODE_TYPE_FILTER = 8; // Filter node NODE_TYPE_LOOP = 9; // Loop node NODE_TYPE_BALANCE = 10; // Balance node for retrieving wallet token balances NODE_TYPE_AWAIT = 11; // Suspendable node: pause until a signal arrives (durable execution) } // ExecutionTier defines value-capture pricing groups for on-chain execution nodes. // Tiers are pure pricing buckets — meaning comes from classification logic, not the label. // Non-execution nodes use UNSPECIFIED (no fee). enum ExecutionTier { EXECUTION_TIER_UNSPECIFIED = 0; // No value-capture fee (non-execution nodes) EXECUTION_TIER_1 = 1; // Pricing group 1 (default: 0.03% of tx value) EXECUTION_TIER_2 = 2; // Pricing group 2 (default: 0.09% of tx value) EXECUTION_TIER_3 = 3; // Pricing group 3 (default: 0.18% of tx value) } enum ExecutionMode { EXECUTION_MODE_SEQUENTIAL = 0; // Run iterations sequentially (default for safety) EXECUTION_MODE_PARALLEL = 1; // Run iterations in parallel } // ============================================================================ // TOKEN METADATA MESSAGES // ============================================================================ // TokenMetadata represents ERC20 token information message TokenMetadata { string id = 1; // Contract address (lowercase, normalized) string name = 2; // Token name (e.g., "USD Coin") string symbol = 3; // Token symbol (e.g., "USDC") uint32 decimals = 4; // Number of decimal places } // GetTokenMetadataReq is the request for token metadata lookup message GetTokenMetadataReq { string address = 1; // Contract address to look up // Chain to look the token up on. 0 = aggregator default chain. int64 chain_id = 2; } // GetTokenMetadataResp is the response containing token metadata message GetTokenMetadataResp { TokenMetadata token = 1; // Token metadata information bool found = 2; // Whether the token was found string source = 3; // Source of data: "whitelist", "rpc", or "cache" } // ============================================================================ // CLIENT-FACING MESSAGES // ============================================================================ // TRIGGER AND NODE INPUT SYSTEM: // // All triggers and nodes now support an `input` field of type google.protobuf.Value, // which stores user-provided input data as JavaScript objects. This data is available // for reference by subsequent nodes during task execution. // // During task execution, both the output data and input data of each step are accessible: // - `node_name.data` or `node_name.output` - The computed output from the node execution // - `node_name.input` - The user-provided input data defined when the task/node was created // // This allows for flexible data flow where: // 1. Static configuration is stored in the Config field // 2. User-provided runtime data is stored in the input field // 3. Computed results are available in the Output field // // Example usage in SimulateTask or CreateTask: // - Define input data for each node/trigger when creating the task // - Reference both computed outputs and input data in subsequent nodes // - Use expressions like `${previous_node.data.result}` and `${previous_node.input.user_value}` message IdReq { string id = 1; } // Lang defines supported languages/formats for code editors and data validation // Following protobuf best practice: 0 = UNSPECIFIED (not set) enum Lang { LANG_UNSPECIFIED = 0; // Not set - application must reject this LANG_JAVASCRIPT = 1; // JavaScript expressions LANG_JSON = 2; // JSON format LANG_GRAPHQL = 3; // GraphQL queries LANG_HANDLEBARS = 4; // Handlebars templates } // Triggers are always the first element in a task execution flow // They have Config (static parameters) but no Input (no preceding nodes) message FixedTimeTrigger { message Config { repeated int64 epochs = 1; } message Output { google.protobuf.Value data = 1; } // Include Config as field so it is generated in Go Config config = 1; } // Simple timebase or cron syntax. message CronTrigger { message Config { repeated string schedules = 1; } message Output { google.protobuf.Value data = 1; } // Include Config as field so it is generated in Go Config config = 1; } message BlockTrigger { message Config { int64 interval = 1; // Chain to watch blocks on. Required: a task carries no chain to inherit. int64 chain_id = 2; } message Output { google.protobuf.Value data = 1; } // Include Config as field so it is generated in Go Config config = 1; } // EventTrigger monitors blockchain events using direct RPC filter queries. // Clients provide an array of ethereum.FilterQuery structures that map directly to RPC calls. // This approach eliminates parsing overhead and provides maximum flexibility and performance. message EventTrigger { // Query represents a single ethereum.FilterQuery for RPC filtering message Query { // Contract addresses to filter events from. Empty means any contract. repeated string addresses = 1; // Topic filters as a flat array of topic values. // Position in array determines topic index: topics[0]=signature, topics[1]=param1, topics[2]=param2, etc. // Use null or empty string for wildcards (any value). // // Example for Transfer events FROM a specific address (TO is any): // topics: [ // "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", // [0] Transfer signature // "0x000000000000000000000000fE66125343Aabda4A330DA667431eC1Acb7BbDA9", // [1] FROM address (padded) // null // [2] TO address (wildcard - any recipient) // ] // // For monitoring transfers FROM OR TO an address, create TWO separate queries: // Query 1: topics[1]=address, topics[2]=null (transfers FROM address) // Query 2: topics[1]=null, topics[2]=address (transfers TO address) repeated string topics = 2; // Maximum number of events this query should process per block/time window // Used for safety - if exceeded, operator notifies aggregator to cancel task optional uint32 max_events_per_block = 3; // Contract ABI as array of ABI elements repeated google.protobuf.Value contract_abi = 4; // Event conditions to evaluate on decoded event data repeated EventCondition conditions = 5; // Method calls for enhanced event data formatting (e.g., decimals, description) repeated MethodCall method_calls = 6; } // Method call configuration for enhanced formatting message MethodCall { string method_name = 1; // Method name (e.g., "decimals") optional string call_data = 2; // Hex-encoded calldata for the method (used when method_params is not provided) repeated string apply_to_fields = 3; // Fields to apply formatting to (e.g., ["current", "answer"]) repeated string method_params = 4; // Array of Handlebars templates for method parameters (e.g. ["{{value.sender}}", "{{value.recipient}}", "{{value.amount}}"]) } message Config { // Array of RPC filter queries. Each query creates a separate subscription. // For FROM-OR-TO scenarios, provide two queries: one for FROM, one for TO. repeated Query queries = 1; // Cooldown period in seconds. After a trigger fires, wait this many seconds before // allowing the same task to trigger again. This prevents repeated firing when // conditions remain true (e.g., price > threshold fires every block). // Default: 300 (5 minutes cooldown - prevents repeated firing when conditions remain true) // Set to 0 to disable cooldown (triggers fire immediately when conditions match) optional uint32 cooldown_seconds = 2; // Chain to watch events on. Required: a task carries no chain to inherit. int64 chain_id = 3; } message Output { google.protobuf.Value data = 1; // Parsed event data as structured value } // Include Config as field so it is generated in Go Config config = 1; } message ManualTrigger { message Config { // User-defined data that will be returned in the Output google.protobuf.Value data = 1; // Headers for webhook testing - map format consistent with REST API nodes map headers = 2; // Path parameters for webhook testing - map format consistent with REST API nodes map pathParams = 3; // Language/format of the data field - REQUIRED for validation // Must be explicitly set (cannot be LANG_UNSPECIFIED). Application rejects zero value. Lang lang = 4; } message Output { // User-defined data from trigger config - this is the main payload for manual triggers google.protobuf.Value data = 1; } // Include Config as field so it is generated in Go Config config = 1; } message TaskTrigger { string name = 1; // NEW: Use the enum for type identification (Phase 1: add alongside existing) TriggerType type = 8; oneof trigger_type { // manual task with proper ManualTrigger structure for webhook testing ManualTrigger manual = 2; // run at a specific epoch, name inspired by unix `at` utility FixedTimeTrigger fixed_time = 3; // interval such as every hour/day/ etc can be converted to cronsyntax by the sdk/studio CronTrigger cron = 4; // currently the only support syntax is every blocks BlockTrigger block = 5; // support filter by event expression such as topic0, topic1, topoc2 and event_data and contract_address EventTrigger event = 6; } string id = 7; } // gRPC internal error code use up to 17, we extend and start from 1000 to avoid any conflict // Guide: https://grpc.io/docs/guides/error/ // Go: https://github.com/grpc/grpc-go/blob/master/codes/codes.go#L199 // Unified error codes for client-server communication // Maps to standard gRPC status codes where applicable, but provides domain-specific error details enum ErrorCode { // Standard success - no error ERROR_CODE_UNSPECIFIED = 0; // 1000-1999: Authentication and Authorization errors UNAUTHORIZED = 1000; // Invalid or missing authentication FORBIDDEN = 1001; // Insufficient permissions INVALID_SIGNATURE = 1002; // Signature verification failed EXPIRED_TOKEN = 1003; // Auth token has expired // 2000-2999: Resource Not Found errors TASK_NOT_FOUND = 2000; // Task/workflow not found EXECUTION_NOT_FOUND = 2001; // Execution not found WALLET_NOT_FOUND = 2002; // Smart wallet not found SECRET_NOT_FOUND = 2003; // Secret not found TOKEN_METADATA_NOT_FOUND = 2004; // Token metadata not found // 3000-3999: Validation and Bad Request errors INVALID_REQUEST = 3000; // General request validation failed INVALID_TRIGGER_CONFIG = 3001; // Trigger configuration is invalid INVALID_NODE_CONFIG = 3002; // Node configuration is invalid INVALID_WORKFLOW = 3003; // Workflow structure is invalid INVALID_ADDRESS = 3004; // Blockchain address format invalid INVALID_SIGNATURE_FORMAT = 3005; // Signature format invalid MISSING_REQUIRED_FIELD = 3006; // Required field is missing // 4000-4999: Resource State errors TASK_ALREADY_EXISTS = 4000; // Task with same ID already exists TASK_ALREADY_COMPLETED = 4001; // Cannot modify completed task TASK_ALREADY_CANCELLED = 4002 [deprecated = true]; // Deprecated: cancelled state replaced by Disabled EXECUTION_IN_PROGRESS = 4003; // Operation not allowed during execution WALLET_ALREADY_EXISTS = 4004; // Wallet already exists for salt SECRET_ALREADY_EXISTS = 4005; // Secret with same name exists // 5000-5999: External Service errors RPC_NODE_ERROR = 5000; // Blockchain RPC node error TENDERLY_API_ERROR = 5001; // Tenderly simulation error TOKEN_LOOKUP_ERROR = 5002; // Token metadata lookup failed SIMULATION_ERROR = 5003; // Workflow simulation failed // 6000-6999: Internal System errors STORAGE_UNAVAILABLE = 6000; // Database/storage system unavailable STORAGE_WRITE_ERROR = 6001; // Failed to write to storage STORAGE_READ_ERROR = 6002; // Failed to read from storage TASK_DATA_CORRUPTED = 6003; // Task data cannot be decoded EXECUTION_ENGINE_ERROR = 6004; // Task execution engine error // 7000-7999: Rate Limiting and Quota errors RATE_LIMIT_EXCEEDED = 7000; // API rate limit exceeded QUOTA_EXCEEDED = 7001; // User quota exceeded TOO_MANY_REQUESTS = 7002; // Too many concurrent requests // 8000-8999: Smart Wallet specific errors SMART_WALLET_RPC_ERROR = 8000; // Smart wallet RPC call failed SMART_WALLET_NOT_FOUND = 8001; // Smart wallet address not found SMART_WALLET_DEPLOYMENT_ERROR = 8002; // Failed to deploy smart wallet INSUFFICIENT_BALANCE = 8003; // Insufficient balance for operation INSUFFICIENT_CREDIT = 8004; // Outstanding value fees exceed credit limit // 9000-9999: Reserved for future use } // TaskStatus represents status of the task. The transition is as follow enum TaskStatus { Enabled = 0; // Task is completed when it reaches its max_execution count or its expiration time Completed = 1; Failed = 2; Running = 4; Disabled = 5; } // ExecutionStatus represents the outcome of a task execution. // // SUCCESS – every executed step succeeded (includes branch/conditional skips). // FAILED – one or more node-level steps failed during execution. // ERROR – system-level failure; the VM could not run the workflow at all. // // Value 4 (formerly PARTIAL_SUCCESS) is reserved and must not be reused. enum ExecutionStatus { EXECUTION_STATUS_UNSPECIFIED = 0; EXECUTION_STATUS_PENDING = 1; EXECUTION_STATUS_SUCCESS = 2; EXECUTION_STATUS_FAILED = 3; reserved 4; reserved "EXECUTION_STATUS_PARTIAL_SUCCESS"; EXECUTION_STATUS_ERROR = 5; // Durable execution: the execution is suspended mid-workflow, waiting for a // signal (chain event, external approval, or timer) to advance. Non-terminal. EXECUTION_STATUS_WAITING = 6; } // Nodes process data from preceding triggers/nodes // They have both Config (static parameters) and Input (runtime variables) message ETHTransferNode { message Config { string destination = 1; string amount = 2; // Chain to execute the transfer on. Required: a task carries no chain to inherit. int64 chain_id = 3; } message Output { google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } message ContractWriteNode { message Config { string contract_address = 1; string call_data = 2; // The ABI as array of ABI elements repeated google.protobuf.Value contract_abi = 3; // Support for multiple method calls in sequence (similar to ContractRead) repeated MethodCall method_calls = 4; // Execution mode for this contract write node: when true (default), use simulation; when false, execute real UserOp optional bool is_simulated = 5; // ETH value to send with the transaction (in wei as string) optional string value = 6; // Custom gas limit for the transaction (as string to handle large numbers) optional string gas_limit = 7; // Chain to execute the write on. Required: a task carries no chain to inherit. int64 chain_id = 8; } message MethodCall { optional string call_data = 1; // Hex-encoded calldata for the method (used when methodParams is not provided) string method_name = 2; // Method name for clarity and response mapping repeated string apply_to_fields = 3; // Fields to apply formatting to (e.g., ["current", "answer"]) repeated string method_params = 4; // Array of Handlebars templates for method parameters (e.g. ["{{value.sender}}", "{{value.recipient}}", "{{value.amount}}"]) } message Output { // Changed from repeated MethodResult to google.protobuf.Value for better JavaScript native type support // Data will be a flattened JSON object with decoded event logs (user-facing results) google.protobuf.Value data = 1; } message MethodResult { string method_name = 1; // The name of the method called google.protobuf.Value method_abi = 2; // Complete ABI entry for this method bool success = 3; // Whether this specific method call succeeded string error = 4; // Error message if failed (empty if success) google.protobuf.Value receipt = 5; // Flexible receipt as JSON object optional uint64 block_number = 6; // Block number (duplicate from receipt for convenience) google.protobuf.Value value = 7; // Return value from contract method (null if no return) } // Include Config as field Config config = 1; } message ContractReadNode { message MethodCall { optional string call_data = 1; // Hex-encoded calldata for the method (used when methodParams is not provided) string method_name = 2; // Optional: method name for clarity (e.g. "latestRoundData") repeated string apply_to_fields = 3; // Fields to apply decimal formatting to (e.g. ["answer"]) repeated string method_params = 4; // Handlebars template for method parameters (e.g. "{{value.address}}") } message Config { string contract_address = 1; // The ABI as array of ABI elements repeated google.protobuf.Value contract_abi = 2; // Array of method calls to execute serially repeated MethodCall method_calls = 3; // Chain to read state from. Required: a task carries no chain to inherit. int64 chain_id = 4; } message MethodResult { // Structured data with named fields based on method signature message StructuredField { string name = 1; // Field name from ABI (e.g. "roundId", "answer", "startedAt") string type = 2; // Solidity type (e.g. "uint80", "int256", "uint256") string value = 3; // The actual value as string, client parses based on type } repeated StructuredField data = 1; // Method metadata string method_name = 2; // The name of the method called bool success = 3; // Whether this specific method call succeeded string error = 4; // Error message if the method call failed } message Output { // Changed from repeated MethodResult to google.protobuf.Value for better JavaScript native type support // Data will be a JSON array of method results with flattened key-value structure google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } message GraphQLQueryNode { message Config { // Static configuration set at node creation time string url = 1; string query = 2; map variables = 3; } message Output { // The data is the result of the graphql query. Because this is GraphQL, the data is a json object // The field of the json object is unknown, it depends on the query google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } message RestAPINode { message Config { string url = 1; map headers = 2; string body = 3; string method = 4; // Generic options bag for backend features (e.g., { "summarize": true }) optional google.protobuf.Value options = 5; } message Output { // Changed from google.protobuf.Any to google.protobuf.Value for better JavaScript native type support google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } message CustomCodeNode { message Config { Lang lang = 1; // Language is static config set at creation time string source = 2; // Source code } message Output { // Changed from google.protobuf.Any to google.protobuf.Value for better JavaScript native type support google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } message BalanceNode { message Config { // Wallet address to check balances for string address = 1; // Chain name or chain ID (e.g., "ethereum", "base", 1, 8453) string chain = 2; // Whether to include tokens marked as spam (default: false) bool include_spam = 3; // Whether to include tokens with zero balance (default: false) // // SMART DEFAULT BEHAVIOR: // When token_addresses is provided, this field has special behavior to ensure // explicitly requested tokens are always returned: // - If token_addresses is NOT empty AND include_zero_balances is false (default): // The system automatically enables zero balance inclusion for requested tokens // - If token_addresses is empty: respects the false default // // PROTOBUF LIMITATION: // Since protobuf bool cannot distinguish between "user explicitly set false" and // "default false", when token_addresses is provided, both cases trigger the smart // default behavior. This means: // ✅ Providing token_addresses → always returns those tokens (even with 0 balance) // ⚠️ To filter out zero balances for specific tokens, set this to true explicitly // and filter on the client side (rare use case) bool include_zero_balances = 4; // Filter tokens below this USD value, in cents (default: 0) // Example: 100 = $1.00, 1050 = $10.50 int64 min_usd_value_cents = 5; // Optional list of specific token addresses to fetch balances for // If empty, fetches all tokens. If specified, only returns balances for these tokens. // // NOTE: When token_addresses is provided, include_zero_balances is automatically // enabled (smart default) to ensure all requested tokens are returned, even if // the wallet has never held them or they have zero balance. repeated string token_addresses = 6; } message Output { // Returns an array of token balance objects // All tokens include a tokenAddress field: // - Native tokens (ETH, BNB, etc.): 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE // - Non-native tokens (ERC20): their contract address google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } // AwaitNode pauses the workflow until a wake arrives (durable execution — see // PLAN_DURABLE_EXECUTION.md). Two mutually-exclusive flavors: external-signal (human // approval — the gateway delivers an approve/reject, e.g. from Telegram) and // chain-event (cross-chain Await — an operator observes an on-chain event). Timer // wakes follow. message AwaitNode { message Config { // External-signal wake parameters (human-approval flavor). Used when chain_event // is unset. string channel = 1; // "telegram" | "api" repeated string approvers = 2; // authorized parties; empty ⇒ the workflow owner string prompt = 3; // shown to the approver uint32 timeout_seconds = 4; // safety bound; 0 ⇒ server default (never an unbounded wait) // Chain-event wake (cross-chain flavor). When set, the Await pauses until an // operator covering chain_event.chain_id observes this event — a mid-workflow // EventTrigger (e.g. a bridge arrival on chain B). Reuses the event-trigger // machinery; mutually exclusive with the external-signal fields above. EventTrigger.Config chain_event = 5; } message Output { // The delivered signal (decision + payload), readable by downstream steps. google.protobuf.Value data = 1; } Config config = 1; } message BranchNode { message Condition { string id = 1; string type = 2; string expression = 3; } message Config { repeated Condition conditions = 1; } message Output { // the output of the branch node contains the condition evaluation results // the execution will continue to the next node belong to this condition // In front-end, when rendering the historical execution, we can draw the relationship coming out of the condition that match this id google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } message FilterNode { message Config { // Filter node acts like .select or .filter to pluck out element in an array that evaluate the expression to true string expression = 1; // Template variable that resolves to the array to filter. // e.g., "{{custom_code1.data}}", "{{settings.items}}" string input_variable = 2; } message Output { // the output of the filter node is the filtered array after apply the filter expression. It works similar to filter of javascript google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } // LoopNode currently not support, but we pre-defined to reverse the field id message LoopNode { message Config { // Template variable that resolves to the array to iterate over. // e.g., "{{settings.address_list}}", "{{custom_code1.data}}" string input_variable = 1; // iter_val is the variable name that will hold the current value during each iteration string iter_val = 2; // iter_key is the variable name that will hold the current key/index during each iteration string iter_key = 3; // execution_mode determines whether iterations run in parallel or sequentially // Note: ContractWrite operations always run sequentially regardless of this setting ExecutionMode execution_mode = 4; // Per-iteration timeout in seconds. If an iteration does not complete within // this duration, it is marked as failed. Default: 30 seconds. uint32 iteration_timeout = 5; } // inside the runner, it can access to the current value of the loop iteration through the iter_val/iter_key above oneof runner { // Transfer eth require no calldata etc, just a destination address and an eth amount to be sent ETHTransferNode eth_transfer = 10; // Run one ore more contracts. The call call also be batched with tool like // multicall to wrap many calls. in a contract write, we need to generate signature and send as userops. ContractWriteNode contract_write = 11; // read data fron a target contract ContractReadNode contract_read = 12; // Make call to a graphql endpoint GraphQLQueryNode graphql_data_query = 13 ; // Make call to a HTTP endpoint RestAPINode rest_api = 14; CustomCodeNode custom_code = 15; } message Output { google.protobuf.Value data = 1; } // Include Config as field Config config = 1; } // The edge is relationship or direct between node message TaskEdge { string id = 1 ; string source = 2 ; string target = 3 ; } message TaskNode { string id = 2; string name = 3; // NEW: Use the enum for type identification (Phase 1: add alongside existing) NodeType type = 1; // based on node_type one and only one of these field are set oneof task_type { // Transfer eth require no calldata etc, just a destination address and an eth amount to be sent ETHTransferNode eth_transfer = 10; // Run one ore more contracts. The call call also be batched with tool like // multicall to wrap many calls. in a contract write, we need to generate signature and send as userops. ContractWriteNode contract_write = 11; // read data fron a target contract ContractReadNode contract_read = 12; // Make call to a graphql endpoint GraphQLQueryNode graphql_query = 13; // Make call to a HTTP endpoint RestAPINode rest_api = 14; // CustomCode allow to run arbitraty JavaScript. BranchNode branch = 15; FilterNode filter = 16; LoopNode loop = 17; CustomCodeNode custom_code = 18; // Get token balances for a wallet address BalanceNode balance = 19; // Pause until a signal arrives (durable execution) AwaitNode await = 20; } } message Execution { string id = 1; int64 start_at = 2; // timestamp when execution started (in milliseconds) int64 end_at = 3; // timestamp when execution ended (in milliseconds) ExecutionStatus status = 4; // detailed execution status (success, failed, partial_success, etc.) string error = 5; // index indicates which run this is for the workflow (0-based: 0=1st run, 1=2nd run, etc.) // This helps clients understand execution order without calculating based on timestamps int64 index = 6; // Fees actually charged for this execution (matches EstimateFeesResp format) Fee execution_fee = 7; // Flat platform fee charged {amount, unit: "USD"} repeated NodeCOGS cogs = 9; // Per-node actual gas/API costs {fee: {amount, unit: "WEI"}} ValueFee value_fee = 10; // Value-capture fee charged (post-paid) {fee: {amount, unit: "PERCENTAGE"}} // Durable execution (suspend/resume). Set only while status == WAITING; cleared // on resume to a terminal status. The accumulated `steps` above carry each // completed step's output, so they are the resumable state — these fields just // mark where/why the execution is parked. string resume_node_id = 11; // the suspended step; execution resumes at its successors string wait_reason = 12; // debug/human label for why it is waiting message Step { string id = 1; // Unified type field - can be trigger type (MANUAL, BLOCK, etc) or node type (CUSTOM_CODE, REST_API, etc) string type = 17; string name = 18; bool success = 2; string error = 13; ErrorCode error_code = 31; string log = 12; repeated string inputs = 16; // Configuration data that was set on this trigger/node google.protobuf.Value config = 19; // Optional structured metadata for testing/debugging; not consumed by subsequent nodes google.protobuf.Value metadata = 25; // Optional execution context for runtime flags and extra info (e.g., is_simulated) google.protobuf.Value execution_context = 26; // Gas cost tracking fields for blockchain operations (contract_write, eth_transfer, // and loop nodes containing on-chain runners). Units are in wei for precision. // Empty string means gas data was not available (e.g., receipt unavailable, or // non-on-chain step). For loop steps, these are aggregated from all iterations. string gas_used = 27; // Amount of gas consumed by the transaction string gas_price = 28; // Gas price in wei per gas unit string total_gas_cost = 29; // Total cost (gas_used * gas_price) in wei oneof output_data { // Trigger outputs BlockTrigger.Output block_trigger = 20; FixedTimeTrigger.Output fixed_time_trigger = 21; CronTrigger.Output cron_trigger = 22; EventTrigger.Output event_trigger = 23; ManualTrigger.Output manual_trigger = 24; // Node outputs ETHTransferNode.Output eth_transfer = 3; GraphQLQueryNode.Output graphql = 4; ContractReadNode.Output contract_read = 5; ContractWriteNode.Output contract_write = 6; CustomCodeNode.Output custom_code = 7; RestAPINode.Output rest_api = 8; BranchNode.Output branch = 9; FilterNode.Output filter = 10; LoopNode.Output loop = 11; BalanceNode.Output balance = 30; } // timestamp when step started (in milliseconds) int64 start_at = 14; // timestamp when step ended (in milliseconds) int64 end_at = 15; } repeated Step steps = 8; } message Task { string id = 1; string owner = 2; string smart_wallet_address = 3; // task won't be check before this (timestamp in milliseconds) int64 start_at = 4; // task won't be run/check after this (timestamp in milliseconds) int64 expired_at = 5; // arbitrary data about this task. has a limit of 255 character string name = 6; // timestamp when task was completed (in milliseconds) int64 completed_at = 7; // limit on how many time this task can run. Set to 0 will make it run unlimited until cancelling or reaching its expired time int64 max_execution = 8; // return how many time this task has run int64 execution_count = 9; // timestamp when task was last executed (in milliseconds) int64 last_ran_at = 10; TaskStatus status = 11; TaskTrigger trigger = 12; repeated TaskNode nodes = 13; repeated TaskEdge edges = 14; // Input variables defined at workflow creation time // These variables are available globally to all nodes during execution // and can be referenced using JavaScript template syntax like ${variableName} map input_variables = 15; // Field 16 (chain_id) removed: a task no longer belongs to a chain. Chain // lives only on chain-aware triggers/nodes (each carries its own chain_id); // per-chain views are derived from the parts. See PLAN_CHAIN_DECOUPLING.md (G5). reserved 16; // last_validation_error is the most recent message from a validation // rejection (e.g. "task smart wallet address does not belong to owner"). // Empty when the task has never been rejected, or when a later execution // made it past validation and reset the counter below. string last_validation_error = 17; // consecutive_validation_failures counts trigger ticks where validation // rejected this task with a *permanent* error (bad wallet config, VM // construction failure). Transient errors (RPC timeouts, credit limit) // do not increment. Resets to 0 the next time an execution gets past // validation. Past the threshold defined in core/taskengine/executor.go, // the task is automatically flipped to Disabled. uint32 consecutive_validation_failures = 18; } message CreateTaskReq { TaskTrigger trigger = 1; int64 start_at = 2; int64 expired_at = 3; int64 max_execution = 4; // the smart wallet address that will be used to run this task // When leaving out, we will use the default(salt=0) wallet string smart_wallet_address = 5; string name = 6; repeated TaskNode nodes = 7; repeated TaskEdge edges = 8; // Input variables to be stored with the workflow definition // These variables will be available globally to all nodes during execution // and can be referenced using JavaScript template syntax like ${variableName} map input_variables = 9; // Field 10 (chain_id) removed: a task no longer carries a chain. Each // chain-aware trigger/node specifies its own chain_id (required). See // PLAN_CHAIN_DECOUPLING.md (G5). reserved 10; } message CreateTaskResp { string id = 1; } message NonceRequest { string owner = 1; } message NonceResp { string nonce = 1; } message ListWalletReq { // filter out by factory address or salt // otherwise return all the wallet string factory_address = 1; // TODO: Consider renaming to factoryAddress for consistency if this is used as a filter key matching SmartWallet.factoryAddress string salt = 2; } message SmartWallet { string address = 1; string salt = 2; string factory = 3; bool is_hidden = 4; // Whether the wallet is hidden } message ListWalletResp { repeated SmartWallet items = 1; } message ListTasksReq { // Filter out by the smart_wallet_address repeated string smart_wallet_address = 1; // Get items before this cursor value (for backward pagination) string before = 2; // Get items after this cursor value (for forward pagination) string after = 3; int64 limit = 4; // Field control options for flexible response content bool include_nodes = 5; // Include task nodes (expensive field) bool include_edges = 6; // Include task edges (expensive field) } message ListTasksResp { repeated Task items = 1; PageInfo page_info = 2; } message ListExecutionsReq { repeated string task_ids = 1; // Get items before this cursor value (for backward pagination) string before = 2; // Get items after this cursor value (for forward pagination) string after = 3; int64 limit = 4; } message ListExecutionsResp { repeated Execution items = 1; PageInfo page_info = 2; } message ExecutionReq { string task_id = 1; string execution_id = 2; } message ExecutionStatusResp { ExecutionStatus status = 1; } message GetKeyReq { // The message to sign, provided by GetSignatureFormat string message = 1; // The signature of the message string signature = 2; } message KeyResp { // The user's address derived from the signature string address = 1; // The auth key to include in all future requests string key = 2; // The message that was signed to produce the signature string message = 3; // Expiry of the auth key in seconds since epoch uint64 expiry = 4; } message GetWalletReq { string salt = 1; // this is the factory address for the wallet, when leaving its empty, we will use our default factory address string factory_address = 2; } message GetWalletResp { string address = 1; string salt = 2; string factory_address = 3; bool is_hidden = 4; uint64 total_task_count = 5; uint64 enabled_task_count = 6; uint64 completed_task_count = 7; uint64 failed_task_count = 8; uint64 disabled_task_count = 9; } message SetWalletReq { string salt = 1; // this is the factory address for the wallet, when leaving its empty, we will use our default factory address string factory_address = 2; // whether the wallet should be hidden in getWallets results bool is_hidden = 3; } // Request message for WithdrawFunds operation message WithdrawFundsReq { // The recipient address to send funds to string recipient_address = 1; // The amount to withdraw in wei for ETH or smallest token unit for ERC20 // Use "max" (case-insensitive) to withdraw the entire available balance string amount = 2; // Token type: "ETH" for native ETH, or contract address for ERC20 tokens string token = 3; // Required: Smart wallet address to withdraw from (must be from user's getWallet() call) string smart_wallet_address = 4; // Chain to execute the withdrawal on. 0 = aggregator default chain. int64 chain_id = 5; } // Response message for WithdrawFunds operation message WithdrawFundsResp { bool success = 1; // Whether the operation completed successfully string status = 2; // Status description: "pending", "submitted", "failed" string message = 3; // Human-readable message about what happened string user_op_hash = 4; // UserOperation hash from bundler string transaction_hash = 5; // Blockchain transaction hash (if available) int64 submitted_at = 6; // Unix timestamp when UserOp was submitted string smart_wallet_address = 7; // Smart wallet address used for withdrawal string recipient_address = 8; // Recipient address string amount = 9; // Amount withdrawn string token = 10; // Token type (ETH or contract address) } message TriggerTaskReq { string task_id = 1; // Flattened from TriggerReason: Use the top-level TriggerType enum for consistency TriggerType trigger_type = 2; // Flattened from TriggerReason: trigger output data based on type oneof trigger_output { BlockTrigger.Output block_trigger = 3; FixedTimeTrigger.Output fixed_time_trigger = 4; CronTrigger.Output cron_trigger = 5; EventTrigger.Output event_trigger = 6; ManualTrigger.Output manual_trigger = 7; } // when setting is_blocking=true, the execution run in the same request. the request is blocked until the execution is done // setting to false, the task will be execute in our normal queueu system, and the request won't block. // default value is false, for interact testing, set this to true bool is_blocking = 8; // Input variables for template resolution in trigger configuration (e.g., settings: {runner, chain_id}) // These variables are merged with the workflow's input_variables during execution map trigger_input = 9; // Chain to execute against. 0 = use the task's stored chain_id. int64 chain_id = 10; } message TriggerTaskResp { // Regardless whether it is a block or async, we always get back the same kind of id for this trigger. // The caller then make a second request to GetExecution to check for the execution status and data. // In the blocking mode, the execution_id is materialized and has been created, we can then call GetExecution on it immediately to receive result // In async mode, the execution_id is created ahead of time and not materialized, calling GetExecutionStatus on it will return Status=Pending for example. Once Status=Completed you can call GetExecution to get all log and detail. Call GetExecution before it is completed will result in "Execution Not Found" string execution_id = 1; ExecutionStatus status = 2; // Always return the workflow ID string workflow_id = 3; // Optional execution fields - populated when isBlocking=true or when execution is complete optional int64 start_at = 4; // timestamp when execution started (in milliseconds) - populated when isBlocking=true or when execution is complete, making it available for both blocking and non-blocking modes optional int64 end_at = 5; // timestamp when execution ended (in milliseconds) - only available for blocking // Field 6 (success) was removed - do not reuse this field number optional string error = 7; // error message if execution failed - only available for blocking repeated Execution.Step steps = 8; // execution steps - only available for blocking } message CreateOrUpdateSecretReq { // name of the secret. it should be [a-zA-Z0-9_]+ string name = 1; // value can be any valid unicode string // Secret is the only thing we can change in an update. workflow and org id cannot be change string secret = 2; // A secret when define can be at these level // - org: available to everything in the org. Currently this isn't supported yet. reserve for future use // - user: available to all workflow of an user. This is the default level // - workflow: available to a single workflow. To make a secret available to multiple workflow, either use org/user level or define them on other workflow. string workflow_id = 3; string org_id = 4; } message ListSecretsReq { string workflow_id = 1; // Get items before this cursor value (for backward pagination) string before = 2; // Get items after this cursor value (for forward pagination) string after = 3; int64 limit = 4; // Field control options for flexible response content bool include_timestamps = 5; // Include created_at and updated_at fields bool include_created_by = 6; // Include created_by field bool include_description = 7; // Include description field } // Standard pagination info following GraphQL cursor-based pagination message PageInfo { string start_cursor = 1; // Cursor pointing to the first item in the current page string end_cursor = 2; // Cursor pointing to the last item in the current page bool has_previous_page = 3; // Whether there are more items before the current page bool has_next_page = 4; // Whether there are more items after the current page } // Secret represents a secret configuration without the actual secret value message Secret { // when listing secret, we don't return its value, just secret and last update string name = 1; string scope = 2; string workflow_id = 3; string org_id = 4; // Additional fields that can be controlled via field masks int64 created_at = 5; // Unix timestamp when secret was created int64 updated_at = 6; // Unix timestamp when secret was last updated string created_by = 7; // User ID who created the secret string description = 8; // Optional description of the secret } message ListSecretsResp { repeated Secret items = 1; PageInfo page_info = 2; } message DeleteSecretReq { string name = 1; // Delete the secret belong to the specific workflow. Without its, we delete the one belong to user string workflow_id = 2; // Delete the secret belong to the specific prg. Without its, we delete the one belong to user string org_id = 3; } // Response message for DeleteSecret operation message DeleteSecretResp { bool success = 1; // Whether the operation completed successfully string status = 2; // Status description: "deleted", "not_found", "already_deleted" string message = 3; // Human-readable message about what happened int64 deleted_at = 4; // Unix timestamp when the secret was deleted (if applicable) string secret_name = 5; // Name of the secret that was affected string scope = 6; // Scope of the deleted secret: "user", "workflow", "org" } message GetSignatureFormatReq { // The wallet address to include in the signature format string wallet = 1; } message GetSignatureFormatResp { // The formatted signature message with server-side values filled in string message = 1; } // Response message for CreateSecret operation message CreateSecretResp { bool success = 1; // Whether the operation completed successfully string status = 2; // Status description: "created", "already_exists", "error" string message = 3; // Human-readable message about what happened int64 created_at = 4; // Unix timestamp when the secret was created (if applicable) string secret_name = 5; // Name of the secret that was affected string scope = 6; // Scope of the created secret: "user", "workflow", "org" } // Response message for UpdateSecret operation message UpdateSecretResp { bool success = 1; // Whether the operation completed successfully string status = 2; // Status description: "updated", "not_found", "error" string message = 3; // Human-readable message about what happened int64 updated_at = 4; // Unix timestamp when the secret was updated (if applicable) string secret_name = 5; // Name of the secret that was affected string scope = 6; // Scope of the updated secret: "user", "workflow", "org" } // Response message for DeleteTask operation message DeleteTaskResp { bool success = 1; // Whether the operation completed successfully string status = 2; // Status description: "deleted", "not_found", "cannot_delete" string message = 3; // Human-readable message about what happened int64 deleted_at = 4; // Unix timestamp when the task was deleted (if applicable) string id = 5; // ID of the task that was affected string previous_status = 6; // Previous status of the task before deletion } // Toggle task enabled state message SetTaskEnabledReq { string id = 1; bool enabled = 2; } message SetTaskEnabledResp { bool success = 1; string status = 2; // "enabled" | "disabled" | "not_found" | "error" string message = 3; string id = 4; string previous_status = 5; int64 updated_at = 6; // ms } // The public client `Aggregator` gRPC service has been removed as of // the REST migration — clients use /api/v1/* over HTTP/JSON. The // message types this service used to reference (CreateTaskReq, // GetWalletReq, ListSecretsResp, EstimateFeesReq, etc.) are still // defined below because the engine, REST handlers, and the operator- // facing Node service share them. The Node service (operator stream // + checkin) is the only gRPC interface still exposed. See // API_REST_IMPLEMENTATION_PLAN.md. // Request message for GetWorkflowCount message GetWorkflowCountReq { repeated string addresses = 1; // Optional array of smart wallet addresses } // Response message for GetWorkflowCount message GetWorkflowCountResp { // TODO: eventually to support active, cancel and other metrics int64 total = 1; // the total count of workflow } message GetExecutionCountReq { repeated string workflow_ids = 1; // Optional array of workflow IDs, count all executions of the owner if not provided } // Response message for GetExecutionCount message GetExecutionCountResp { // TODO: eventually to support success, error count execution int64 total = 1; // The total count of executions } // Request message for GetExecutionStats message GetExecutionStatsReq { repeated string workflow_ids = 1; // Optional array of workflow IDs int64 days = 2; // Number of days to look back (default: 7) } // Response message for GetExecutionStats message GetExecutionStatsResp { int64 total = 1; // Total number of executions int64 succeeded = 2; // Number of successful executions int64 failed = 3; // Number of failed executions double avg_execution_time = 4; // Average execution time in milliseconds } // Request message for RunNodeWithInputs message RunNodeWithInputsReq { TaskNode node = 1; // Complete node definition with proper Config (consistent with SimulateTask) // Field 2 was node_config (removed in Jan 2025) - do not reuse map input_variables = 3; // Input variables for the node // Chain to run the node against. 0 = aggregator default chain. // node.config.chain_id (if non-zero) takes precedence over this field. int64 chain_id = 4; // Optional ERC20 balance/allowance state overrides applied only during this // isolated node simulation (RunNodeImmediately). Lets callers seed token // balances and approvals so contract-write simulations (e.g. Uniswap swaps) // don't revert with "transfer amount exceeds allowance/balance" before the // approval/funding transactions have been run. Simulation-only: a // real-execution request (isSimulated=false) that sets these is rejected with // an error, never silently ignored. repeated ERC20StateOverride erc20_overrides = 5; } // ERC20StateOverride seeds a token's balanceOf / allowance storage slots for a // single simulation. The slot for balanceOf[owner] is // keccak256(abi.encode(owner, balance_slot)); the slot for // allowance[owner][spender] is // keccak256(abi.encode(spender, keccak256(abi.encode(owner, allowance_slot)))). message ERC20StateOverride { string token_address = 1; // ERC20 token contract address string owner_address = 2; // Address whose balance/allowance to override optional string spender_address = 3; // Spender to approve (required for allowance override) optional string balance = 4; // Balance override (hex 0x… or decimal string) optional string allowance = 5; // Allowance override (hex 0x… or decimal string) optional uint64 balance_slot = 6; // Storage slot for the balanceOf mapping (required when balance is set; layout varies per token) optional uint64 allowance_slot = 7; // Storage slot for the allowance mapping (required when allowance is set; layout varies per token) } // Response message for RunNodeWithInputs message RunNodeWithInputsResp { bool success = 1; // Whether the execution was successful string error = 3; // Error message if execution failed // Field 4 (node_id) was removed in Aug 2025 - do not reuse this field number google.protobuf.Value metadata = 5; // Optional structured metadata for testing/debugging // Optional execution context for runtime flags and extra info (e.g., is_simulated) google.protobuf.Value execution_context = 6; ErrorCode error_code = 7; // Structured error code for better client-side error handling // Use specific output types for nodes only oneof output_data { // Node outputs ETHTransferNode.Output eth_transfer = 10; GraphQLQueryNode.Output graphql = 11; ContractReadNode.Output contract_read = 12; ContractWriteNode.Output contract_write = 13; CustomCodeNode.Output custom_code = 14; RestAPINode.Output rest_api = 15; BranchNode.Output branch = 16; FilterNode.Output filter = 17; LoopNode.Output loop = 18; BalanceNode.Output balance = 19; } } // Request message for RunTrigger message RunTriggerReq { TaskTrigger trigger = 1; // Complete trigger definition with proper Config (consistent with SimulateTask) // Field 2 was trigger_config (removed in Jan 2025) - do not reuse map trigger_input = 3; // Input data for the trigger } // Response message for RunTrigger message RunTriggerResp { bool success = 1; // Whether the execution was successful string error = 2; // Error message if execution failed // Field 3 (trigger_id) was removed in Aug 2025 - do not reuse this field number google.protobuf.Value metadata = 4; // Optional structured metadata for testing/debugging // Optional execution context for runtime flags and extra info (e.g., is_simulated) google.protobuf.Value execution_context = 5; ErrorCode error_code = 6; // Structured error code for better client-side error handling // Use specific output types for triggers oneof output_data { BlockTrigger.Output block_trigger = 10; FixedTimeTrigger.Output fixed_time_trigger = 11; CronTrigger.Output cron_trigger = 12; EventTrigger.Output event_trigger = 13; ManualTrigger.Output manual_trigger = 14; } } // Request message for SimulateTask message SimulateTaskReq { // Complete task definition for simulation (no need to save to storage first) TaskTrigger trigger = 1; // The trigger configuration repeated TaskNode nodes = 2; // All workflow nodes repeated TaskEdge edges = 3; // All edges connecting the nodes map input_variables = 6; // Input variables for the simulation // Chain to simulate against. 0 = aggregator default chain. // Per-node / per-trigger chain_id (if non-zero) takes precedence. int64 chain_id = 7; } // Request message for EstimateFees message EstimateFeesReq { // Complete workflow definition for fee estimation (similar to SimulateTaskReq) TaskTrigger trigger = 1; // The trigger configuration repeated TaskNode nodes = 2; // All workflow nodes repeated TaskEdge edges = 3; // All edges connecting the nodes // Workflow lifecycle parameters int64 created_at = 4; // Timestamp when workflow will be created (milliseconds) int64 expire_at = 5; // Timestamp when workflow will expire (milliseconds) int64 max_execution = 6; // Maximum number of executions allowed // Smart wallet runner (optional - if not provided, extracted from input_variables.settings.runner) string runner = 7; // Smart wallet address to use for gas estimation // Input variables for workflow execution simulation // Should contain settings with at least runner for fee estimation map input_variables = 8; // Chain to estimate fees against. 0 = aggregator default chain. int64 chain_id = 9; } // Fee amount in both native token and USD message FeeAmount { string native_token_amount = 1; // Amount in native token (wei for ETH) string native_token_symbol = 2; // Native token symbol (e.g., "ETH", "BNB", "MATIC") string usd_amount = 3; // Equivalent amount in USD (with 2 decimal places) string ap_token_amount = 4; // Future: Amount in AP tokens } // Gas fee breakdown for blockchain operations message GasFeeBreakdown { FeeAmount total_gas_fees = 1; // Total estimated gas costs // Per-operation breakdown repeated GasOperationFee operations = 2; // Gas estimation metadata string gas_price_gwei = 3; // Current gas price in Gwei string total_gas_units = 4; // Total estimated gas units bool estimation_accurate = 5; // Whether estimation used real RPC or fallback string estimation_method = 6; // "rpc_estimate" or "tenderly_simulation" or "fallback" } // Individual gas operation fee message GasOperationFee { string operation_type = 1; // "contract_write", "eth_transfer", "smart_wallet_creation" string node_id = 2; // Associated node ID string method_name = 3; // Contract method name (for contract_write) FeeAmount fee = 4; // Fee for this operation string gas_units = 5; // Estimated gas units for this operation } // Smart wallet creation fees message SmartWalletCreationFee { bool creation_required = 1; // Whether new smart wallet deployment is needed FeeAmount creation_fee = 2; // Gas cost for factory.createAccount() FeeAmount initial_funding = 3; // Recommended initial funding amount string wallet_address = 4; // Predicted or existing wallet address } // Unit-safe fee value. Every monetary field is self-describing. // Units: "USD" (fiat), "WEI" (native token smallest unit), "PERCENTAGE" (0.03 = 0.03%) message Fee { string amount = 1; // Numeric value as string (precision-safe) string unit = 2; // "USD", "WEI", "PERCENTAGE" } // Native token metadata for the chain message NativeToken { string symbol = 1; // e.g., "ETH" int32 decimals = 2; // e.g., 18 } // Per-node cost of goods sold (gas, external API costs, etc.) message NodeCOGS { string node_id = 1; string cost_type = 2; // canonical REST values: "gas", "externalApi", "walletCreation" — see core/taskengine/fee_enums.go and OpenAPI enum NodeCOGSCostType Fee fee = 3; // Cost in WEI string gas_units = 4; // Gas units (for gas costs only) } // Workflow-level value-capture fee. // Single fee for the entire workflow based on what it does (not per-node). message ValueFee { Fee fee = 1; // { amount: "0.03", unit: "PERCENTAGE" } ExecutionTier tier = 2; // Pricing group (TIER_1, TIER_2, TIER_3) string value_base = 3; // What the percentage applies to (e.g., "input_token_value") string classification_method = 4; // canonical REST values: "ruleBased" (V1) or "llm" (V2) — see core/taskengine/fee_enums.go and OpenAPI enum ValueFeeClassificationMethod float confidence = 5; // Classification confidence (0.0–1.0) string reason = 6; // Why this tier was assigned } // Promotional discount message FeeDiscount { string discount_type = 1; // canonical REST values: "newUser", "volume", "promotional", "betaProgram" — see core/taskengine/fee_enums.go and OpenAPI enum FeeDiscountDiscountType string discount_name = 2; Fee discount = 3; // Discount amount or percentage string expiry_date = 4; string terms = 5; } // Response message for EstimateFees // All fees are per-execution. No totals — client computes. // Components: execution_fee (USD) + cogs[] (WEI) + value_fee (PERCENTAGE) message EstimateFeesResp { bool success = 1; string error = 2; ErrorCode error_code = 3; // Chain and token context string chain_id = 4; NativeToken native_token = 5; // Flat per-execution platform fee Fee execution_fee = 6; // Cost of goods sold — per-node operational costs (gas, external API) repeated NodeCOGS cogs = 7; // Workflow-level value-capture fee (single, not per-node) ValueFee value_fee = 8; // Discounts (client sums if needed) repeated FeeDiscount discounts = 9; // Pricing metadata string pricing_model = 10; // "v1" repeated string warnings = 11; } // EventCondition represents a condition to evaluate on decoded event data. // MIRRORED in api/openapi.yaml (components.schemas.EventCondition). The two // schemas must agree on every field's type — `value` in particular MUST stay // `string` in both. Past schema drift here (OpenAPI authored as an object, // proto as a string) broke production simulate requests; see PR #601. message EventCondition { string field_name = 1; // Event field name (e.g., "answer", "roundId") string operator = 2; // Comparison operator: "gt", "gte", "lt", "lte", "eq", "ne" string value = 3; // Value to compare against (as string, parsed based on type) string field_type = 4; // Field type: "uint256", "int256", "address", "bool", "bytes32", etc. }