Skip to main content

Recipe System Design

Overview

The Recipe System enables composable workflows by chaining multiple actions together with conditional branching, parallel execution, and credit reservation. Recipes allow users to define multi-step file processing pipelines that execute atomically with proper error handling and rollback.

Core Concepts

What is a Recipe?

A Recipe is a directed acyclic graph (DAG) of actions with:

  • Steps: Individual action invocations with input/output bindings
  • Conditions: Branch logic based on intermediate results or file metadata
  • Parallel Branches: Concurrent execution paths that can merge
  • Credit Reservation: Upfront cost estimation with actual usage tracking

Recipe vs Action

AspectActionRecipe
ScopeSingle operationMultiple operations
ExecutionSynchronous or asyncAlways async
CreditsImmediate deductionReserved upfront, committed on completion
InputSingle file setSingle file set, transformed between steps
OutputSingle resultAggregated results from all steps

Architecture

Component Diagram

┌─────────────────────────────────────────────────────────────────┐
│ Gateway │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Recipe │ │ Recipe │ │ Recipe │ │
│ │ Registry │──│ Engine │──│ Credit Manager │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Action │◄─────────────┘ │
│ │ │ Executor │ │
│ │ └─────────────┘ │
└─────────│───────────────│───────────────────────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│Firestore │ │ Workers │
│(recipes) │ │ │
└──────────┘ └──────────┘

Data Flow

1. User submits recipe execution request


2. Gateway estimates total credits


3. Credits reserved (not deducted)


4. Recipe engine starts execution


5. Each step executes via Action Executor

├──► On step success: track actual credits

└──► On step failure: stop execution, release reservation


6. All steps complete


7. Commit actual credits, release excess reservation


8. Return aggregated results

Recipe Definition Schema

TypeScript Interface

interface Recipe {
id: string;
name: string;
version: string;
description: string;
category: string;

// Recipe structure
steps: RecipeStep[];

// Input/output contracts
inputSchema: JSONSchema;
outputSchema: JSONSchema;

// Cost estimation
creditEstimation: CreditEstimation;

// Metadata
createdAt: string;
updatedAt: string;
createdBy: string;
status: "draft" | "active" | "deprecated";
}

interface RecipeStep {
id: string;
name: string;
type: "action" | "condition" | "parallel" | "transform";

// Action step
action?: ActionReference;

// Condition step (if/then/else branching)
condition?: {
expression: string; // CEL expression
then: RecipeStep[];
else?: RecipeStep[];
};

// Parallel step (concurrent execution)
parallel?: {
branches: RecipeStep[][];
waitFor: "all" | "any" | "first_success";
failureStrategy: "fail_fast" | "continue" | "ignore";
};

// Transform step (data manipulation without action call)
transform?: {
expression: string; // CEL expression for data transformation
outputBinding: string;
};

// Input/output bindings
inputBindings?: Record<string, string>; // Maps step inputs to previous outputs
outputBinding?: string; // Name for this step's output

// Error handling
onError?: ErrorHandler;
retryPolicy?: RetryPolicy;
timeout?: string; // Duration string, e.g., "5m"
}

interface ActionReference {
name: string;
version?: string; // If omitted, uses active version
parameters?: Record<string, any>;
}

interface ErrorHandler {
strategy: "fail" | "skip" | "fallback" | "retry";
fallbackStep?: RecipeStep;
maxRetries?: number;
retryDelay?: string;
}

interface RetryPolicy {
maxAttempts: number;
initialDelay: string;
maxDelay: string;
backoffMultiplier: number;
retryableErrors?: string[]; // Error codes that trigger retry
}

interface CreditEstimation {
strategy: "fixed" | "per_file" | "calculated";
fixedCredits?: number;
perFileCredits?: number;
formula?: string; // CEL expression for complex calculations
maxCredits: number; // Upper bound for reservation
}

JSON Example

{
"id": "recipe_pdf_to_translated_markdown",
"name": "pdf-to-translated-markdown",
"version": "1.0.0",
"description": "Convert PDF to Markdown and translate to target language",
"category": "document-processing",

"steps": [
{
"id": "step_1",
"name": "inspect-file",
"type": "action",
"action": {
"name": "file-inspect",
"version": "1.0.0"
},
"outputBinding": "inspection"
},
{
"id": "step_2",
"name": "check-page-count",
"type": "condition",
"condition": {
"expression": "inspection.extended.pageCount <= 100",
"then": [
{
"id": "step_2a",
"name": "convert-pdf",
"type": "action",
"action": {
"name": "pdf-to-markdown",
"parameters": {
"preserveFormatting": true
}
},
"outputBinding": "markdown"
}
],
"else": [
{
"id": "step_2b",
"name": "fail-large-document",
"type": "transform",
"transform": {
"expression": "'Document too large: ' + string(inspection.extended.pageCount) + ' pages'",
"outputBinding": "error"
}
}
]
}
},
{
"id": "step_3",
"name": "translate",
"type": "action",
"action": {
"name": "translate-text",
"parameters": {
"targetLanguage": "{{parameters.targetLanguage}}"
}
},
"inputBindings": {
"text": "markdown.content"
},
"outputBinding": "translated"
}
],

"inputSchema": {
"type": "object",
"required": ["fileHashes", "targetLanguage"],
"properties": {
"fileHashes": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 1
},
"targetLanguage": {
"type": "string",
"enum": ["es", "fr", "de", "ja", "zh"]
}
}
},

"outputSchema": {
"type": "object",
"properties": {
"translatedFileHash": { "type": "string" },
"originalPageCount": { "type": "integer" },
"wordCount": { "type": "integer" }
}
},

"creditEstimation": {
"strategy": "calculated",
"formula": "10 + (inspection.extended.pageCount * 5) + (inspection.sizeBytes / 1048576 * 2)",
"maxCredits": 1000
}
}

Condition Expressions (CEL)

Recipes use Common Expression Language (CEL) for condition evaluation and data transformation.

Available Variables

VariableTypeDescription
inputobjectOriginal recipe input
parametersobjectUser-provided parameters
fileHashesstring[]Input file hashes
{stepBinding}objectOutput from a previous step (by binding name)
_contextobjectExecution context (accountId, correlationId, etc.)

Example Expressions

// Check file type
inspection.mimeType == "application/pdf"

// Check page count range
inspection.extended.pageCount > 0 && inspection.extended.pageCount <= 50

// Check file size (under 10MB)
inspection.sizeBytes < 10485760

// String operations
inspection.mimeType.startsWith("image/")

// Conditional value
inspection.extended.pageCount > 10 ? "large" : "small"

// List operations
size(input.fileHashes) == 1

// Null checking
has(inspection.extended) && has(inspection.extended.pageCount)

Credit Model for Recipes

Reservation Flow

┌─────────────────────────────────────────────────────────────────┐
│ Credit Lifecycle │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. ESTIMATE │
│ └─► Calculate max possible credits based on formula │
│ │
│ 2. RESERVE │
│ └─► Hold max credits (not deducted from balance) │
│ └─► Create reservation record with TTL │
│ │
│ 3. TRACK (per step) │
│ └─► Record actual credits used │
│ └─► Update running total │
│ │
│ 4a. COMMIT (on success) │
│ └─► Deduct actual total from balance │
│ └─► Release reservation │
│ └─► Refund = reserved - actual │
│ │
│ 4b. RELEASE (on failure) │
│ └─► Deduct credits for completed steps only │
│ └─► Release remaining reservation │
│ └─► Record partial execution │
│ │
└─────────────────────────────────────────────────────────────────┘

Credit Estimation Strategies

Fixed Credits

Simple flat rate regardless of input:

{
"strategy": "fixed",
"fixedCredits": 50
}

Per-File Credits

Scales with number of input files:

{
"strategy": "per_file",
"perFileCredits": 25
}

Calculated Credits

CEL expression for complex calculations:

{
"strategy": "calculated",
"formula": "base_cost + (page_count * per_page_cost) + (size_mb * per_mb_cost)",
"maxCredits": 500
}

Failure Policies

PolicyBehaviorCredit Handling
refund_allFull refund on any failureReserved → Released
refund_unusedCharge for completed stepsActual used → Deducted, Rest → Released
no_refundCharge full estimateReserved → Committed (minus buffer)

Parallel Execution

Branch Types

Wait for All

All branches must complete successfully:

{
"type": "parallel",
"parallel": {
"branches": [
[
{
"action": {
"name": "resize-image",
"parameters": { "size": "thumbnail" }
}
}
],
[
{
"action": {
"name": "resize-image",
"parameters": { "size": "medium" }
}
}
],
[
{
"action": {
"name": "resize-image",
"parameters": { "size": "large" }
}
}
]
],
"waitFor": "all",
"failureStrategy": "fail_fast"
}
}

Wait for Any

Continue when first branch completes:

{
"parallel": {
"branches": [...],
"waitFor": "any",
"failureStrategy": "continue"
}
}

Wait for First Success

Continue when first branch succeeds (ignore failures):

{
"parallel": {
"branches": [...],
"waitFor": "first_success",
"failureStrategy": "ignore"
}
}

Failure Strategies

StrategyBehavior
fail_fastCancel other branches on first failure
continueWait for all branches regardless of failures
ignoreIgnore failures, only care about successes

API Endpoints

Recipe Management

POST   /v1/recipes                           Create a new recipe
GET /v1/recipes List recipes (paginated)
GET /v1/recipes/{id} Get recipe by ID
PUT /v1/recipes/{id} Update recipe
DELETE /v1/recipes/{id} Delete recipe (soft delete)
POST /v1/recipes/{id}/validate Validate recipe without saving

Recipe Execution

POST   /v1/recipes/{id}/execute              Execute a recipe
GET /v1/recipes/{id}/estimate Estimate execution cost
GET /v1/recipe-executions/{executionId} Get execution status
GET /v1/recipe-executions List executions (paginated)
POST /v1/recipe-executions/{id}/cancel Cancel running execution

Request/Response Examples

Create Recipe

POST /v1/recipes
Content-Type: application/json
Authorization: Bearer <api_key>

{
"name": "image-enhancement-pipeline",
"version": "1.0.0",
"description": "Upscale and enhance images",
"steps": [...],
"inputSchema": {...},
"outputSchema": {...},
"creditEstimation": {...}
}
HTTP/1.1 201 Created
Content-Type: application/json

{
"id": "recipe_abc123",
"name": "image-enhancement-pipeline",
"version": "1.0.0",
"status": "active",
"createdAt": "2024-01-15T10:30:00Z"
}

Execute Recipe

POST /v1/recipes/recipe_abc123/execute
Content-Type: application/json
Authorization: Bearer <api_key>

{
"fileHashes": ["sha256_abc..."],
"parameters": {
"upscaleFactor": 4,
"enhanceType": "photo"
},
"async": true
}
HTTP/1.1 202 Accepted
Content-Type: application/json

{
"executionId": "exec_xyz789",
"status": "running",
"creditsReserved": 150,
"estimatedDuration": "PT2M30S",
"progressUrl": "/v1/recipe-executions/exec_xyz789"
}

Get Execution Status

GET /v1/recipe-executions/exec_xyz789
Authorization: Bearer <api_key>
HTTP/1.1 200 OK
Content-Type: application/json

{
"id": "exec_xyz789",
"recipeId": "recipe_abc123",
"status": "running",
"progress": {
"currentStep": "step_2",
"completedSteps": ["step_1"],
"percentComplete": 40
},
"credits": {
"reserved": 150,
"used": 45,
"estimated": 120
},
"stepResults": {
"step_1": {
"status": "completed",
"outputBinding": "inspection",
"duration": "PT1.5S",
"creditsUsed": 5
}
},
"startedAt": "2024-01-15T10:30:00Z"
}

Error Handling

Error Types

Error CodeDescriptionRetry?
RECIPE_NOT_FOUNDRecipe ID doesn't existNo
RECIPE_INVALIDRecipe validation failedNo
INSUFFICIENT_CREDITSNot enough credits for reservationNo
STEP_FAILEDIndividual step execution failedConfigurable
STEP_TIMEOUTStep exceeded timeoutConfigurable
CONDITION_ERRORCEL expression evaluation failedNo
ACTION_NOT_FOUNDReferenced action doesn't existNo
EXECUTION_CANCELLEDUser cancelled executionNo
INTERNAL_ERRORUnexpected system errorYes

Step-Level Error Handling

{
"id": "step_with_retry",
"type": "action",
"action": { "name": "flaky-service" },
"retryPolicy": {
"maxAttempts": 3,
"initialDelay": "1s",
"maxDelay": "30s",
"backoffMultiplier": 2.0,
"retryableErrors": ["TIMEOUT", "SERVICE_UNAVAILABLE"]
},
"onError": {
"strategy": "fallback",
"fallbackStep": {
"type": "action",
"action": { "name": "backup-service" }
}
},
"timeout": "5m"
}

Recipe Metadata for Actions

Actions participating in recipes declare their composition capabilities:

type RecipeMetadata struct {
// Type declarations for input/output
InputTypes []TypeDefinition `json:"inputTypes"`
OutputTypes []TypeDefinition `json:"outputTypes"`

// Conditions this action can evaluate
BranchConditions []BranchCondition `json:"branchConditions,omitempty"`

// Whether action can run in parallel
Parallelizable bool `json:"parallelizable"`

// Estimated duration range
DurationRange DurationRange `json:"durationRange,omitempty"`
}

type TypeDefinition struct {
Name string `json:"name"`
MimeTypes []string `json:"mimeTypes,omitempty"`
Schema JSONSchema `json:"schema,omitempty"`
Description string `json:"description"`
}

type BranchCondition struct {
Name string `json:"name"`
Expression string `json:"expression"` // CEL expression
Description string `json:"description"`
}

Example Action with Recipe Metadata

{
"name": "file-inspect",
"version": "1.0.0",
"recipeMetadata": {
"inputTypes": [
{
"name": "any-file",
"mimeTypes": ["*/*"],
"description": "Any file type"
}
],
"outputTypes": [
{
"name": "file-metadata",
"schema": {
"type": "object",
"properties": {
"mimeType": { "type": "string" },
"sizeBytes": { "type": "integer" },
"extended": { "type": "object" }
}
},
"description": "File metadata including MIME type and extended info"
}
],
"branchConditions": [
{
"name": "is-pdf",
"expression": "output.mimeType == 'application/pdf'",
"description": "Check if file is a PDF"
},
{
"name": "is-image",
"expression": "output.mimeType.startsWith('image/')",
"description": "Check if file is an image"
},
{
"name": "is-small",
"expression": "output.sizeBytes < 10485760",
"description": "Check if file is under 10MB"
}
],
"parallelizable": true,
"durationRange": {
"min": "100ms",
"max": "5s"
}
}
}

Firestore Schema

Collections

recipes/
{recipeId}/
- name: string
- version: string
- description: string
- category: string
- steps: array<Step>
- inputSchema: object
- outputSchema: object
- creditEstimation: object
- status: "draft" | "active" | "deprecated"
- createdAt: timestamp
- updatedAt: timestamp
- createdBy: string (accountId)

recipeExecutions/
{executionId}/
- recipeId: string
- accountId: string
- status: "pending" | "running" | "completed" | "failed" | "cancelled"
- input: object
- currentStep: string
- completedSteps: array<string>
- stepResults: map<string, StepResult>
- creditsReserved: number
- creditsUsed: number
- reservationId: string
- startedAt: timestamp
- completedAt: timestamp
- error: object (if failed)

Implementation Phases

Phase 1: Foundation

  • Firestore schema for recipes and executions
  • Recipe validation logic
  • CEL expression evaluation
  • Credit estimation and reservation

Phase 2: Execution Engine

  • Sequential step execution
  • Input/output binding resolution
  • Condition evaluation
  • Progress tracking and status updates

Phase 3: Advanced Features

  • Parallel execution
  • Retry policies
  • Error handlers and fallbacks
  • Execution cancellation

Phase 4: API & UI

  • REST API endpoints
  • WebSocket progress streaming
  • Recipe builder UI (optional)
  • Execution monitoring dashboard

Security Considerations

  1. CEL Expression Sandboxing: CEL expressions run in a sandboxed environment with no access to system resources

  2. Credit Limits: Maximum credit reservation prevents abuse; requires sufficient balance

  3. Execution Quotas: Per-account limits on concurrent recipe executions

  4. Recipe Ownership: Recipes are scoped to accounts; no cross-account access

  5. Action Access: Recipes can only reference actions the account has access to

  6. Timeout Enforcement: All steps have configurable timeouts with hard limits

Monitoring & Observability

Metrics

  • recipe_executions_total - Total recipe executions by status
  • recipe_execution_duration_seconds - Execution time histogram
  • recipe_step_duration_seconds - Per-step execution time
  • recipe_credits_reserved - Credits reserved per execution
  • recipe_credits_used - Actual credits consumed

Logging

{
"level": "info",
"msg": "recipe step completed",
"executionId": "exec_xyz789",
"recipeId": "recipe_abc123",
"stepId": "step_2",
"stepName": "convert-pdf",
"duration": "1.5s",
"creditsUsed": 15,
"outputBinding": "markdown"
}

Tracing

Recipe executions create a trace with spans for each step, enabling end-to-end visibility across action calls.