Skip to Content
GuideConceptsExecution Model

Execution Model

Understanding Probe’s execution model is crucial for designing efficient workflows and troubleshooting execution issues. This guide explores how Probe schedules, executes, and manages workflow components from start to finish.

Execution Overview

Probe follows a structured execution model that processes workflows in a predictable, deterministic manner:

  1. Workflow Parsing: Parse and validate YAML configuration
  2. Dependency Resolution: Build execution graph based on job dependencies
  3. Job Scheduling: Schedule jobs for execution based on dependencies
  4. Step Execution: Execute steps sequentially within each job
  5. State Management: Track execution state and results
  6. Resource Cleanup: Clean up resources after execution

Execution Hierarchy

The nesting decides what runs at the same time: jobs side by side, steps in order within each one.

Workflow ├── Job 1 (independent) │ ├── Step 1.1 (sequential) │ ├── Step 1.2 (sequential) │ └── Step 1.3 (sequential) ├── Job 2 (independent, parallel to Job 1) │ ├── Step 2.1 (sequential) │ └── Step 2.2 (sequential) └── Job 3 (depends on Job 1 and Job 2) ├── Step 3.1 (sequential) └── Step 3.2 (sequential)

Job Execution Model

Jobs without a needs declaration run at the same time. Adding needs is what turns that set into an ordered graph. A job starts as soon as the jobs it needs are done, without waiting for the other jobs that are running.

Independent Job Execution

Jobs without dependencies execute in parallel:

name: Parallel Service Check description: Check multiple services simultaneously jobs: - id: database-check name: Database Health steps: - name: Check Database uses: http with: method: GET url: "{{vars.DB_URL}}/health" test: res.code == 200 - id: api-check name: API Health steps: - name: Check API uses: http with: method: GET url: "{{vars.API_URL}}/health" test: res.code == 200 - id: cache-check name: Cache Health steps: - name: Check Cache uses: http with: method: GET url: "{{vars.CACHE_URL}}/health" test: res.code == 200

Execution Timeline:

Time 0: Start database-check, api-check, cache-check simultaneously Time T: All jobs complete (T = max execution time of all jobs)

Dependent Job Execution

Jobs with dependencies wait for prerequisite jobs to complete:

name: Staged Deployment Validation description: Validate deployment in dependency order jobs: - id: infrastructure name: Infrastructure Check steps: - name: Database Connectivity id: infrastructure uses: http with: method: GET url: "{{vars.DB_URL}}/ping" test: res.code == 200 outputs: db_healthy: res.code == 200 - id: services name: Service Check needs: [infrastructure] steps: - name: API Service uses: http with: method: GET url: "{{vars.API_URL}}/health" test: res.code == 200 - id: integration name: Integration Test needs: [services] steps: - name: End-to-End Test uses: http with: method: GET url: "{{vars.API_URL}}/integration-test" test: res.code == 200 - id: notification name: Send Notification needs: [integration] steps: - name: Notify Success uses: hello echo: "Deployment validation completed successfully"

Execution Timeline:

Time 0: Start infrastructure job Time T1: infrastructure completes → start services job Time T2: services completes → start integration job Time T3: integration completes → start notification job Time T4: notification completes → workflow done

Complex Dependency Graphs

Jobs can have multiple dependencies and form complex execution graphs:

jobs: # Foundation layer (parallel) - id: database-setup name: Database Setup steps: - name: Initialize Database id: database-setup outputs: db_session_id: "{{random_str(16)}}" - id: cache-setup name: Cache Setup steps: - name: Initialize Cache id: cache-setup outputs: cache_session_id: "{{random_str(16)}}" # Service layer (depends on foundation) - id: user-service name: User Service Test needs: [database-setup, cache-setup] steps: - name: Test User Service id: user-service outputs: user_service_ready: true - id: order-service name: Order Service Test needs: [database-setup] # Only needs database steps: - name: Test Order Service id: order-service outputs: order_service_ready: true # Integration layer (depends on services) - id: integration-test name: Integration Test needs: [user-service, order-service] steps: - name: Test Service Integration uses: hello echo: "Testing integration between user and order services" # Reporting layer (depends on everything) - id: final-report name: Final Report needs: [integration-test] steps: - name: Generate Report uses: hello echo: | Execution Report: Database Setup: {{outputs['database-setup'] ? "✅" : "❌"}} Cache Setup: {{outputs['cache-setup'] ? "✅" : "❌"}} User Service: {{outputs['user-service'] ? "✅" : "❌"}} Order Service: {{outputs['order-service'] ? "✅" : "❌"}}

Execution Timeline:

Time 0: Start database-setup, cache-setup (parallel) Time T1: Both foundation jobs complete → start user-service, order-service Time T2: Both service jobs complete → start integration-test Time T3: integration-test completes → start final-report Time T4: final-report completes → workflow done

Step Execution Model

Within a job, steps run one after another in the order they are written, and a step can be skipped by its condition.

Sequential Step Execution

Within a job, steps execute sequentially in the order defined:

jobs: - id: user-workflow name: User Management Workflow steps: - name: Step 1 - Create User id: create uses: http with: url: "{{vars.API_URL}}/users" method: POST body: '{"name": "Test User", "email": "test@example.com"}' test: res.code == 201 outputs: user_id: res.body.user.id - name: Step 2 - Verify User id: verify uses: http with: method: GET url: "{{vars.API_URL}}/users/{{outputs.create.user_id}}" test: res.code == 200 outputs: user_verified: true - name: Step 3 - Update User id: update uses: http with: url: "{{vars.API_URL}}/users/{{outputs.create.user_id}}" method: PUT body: '{"name": "Updated User"}' test: res.code == 200 - name: Step 4 - Delete User uses: http with: url: "{{vars.API_URL}}/users/{{outputs.create.user_id}}" method: DELETE test: res.code == 204 - name: Step 5 - Confirm Deletion uses: http with: method: GET url: "{{vars.API_URL}}/users/{{outputs.create.user_id}}" test: res.code == 404

Step Execution Order:

Step 1 → Step 2 → Step 3 → Step 4 → Step 5

Each step waits for the previous step to complete before starting.

Conditional Step Execution

Steps still run in order; skipif only decides whether each one executes. The expression sees the same context as test, including the outputs of earlier steps.

steps: - name: Primary Health Check id: primary uses: http with: method: GET url: "{{vars.primary_url}}/health" outputs: primary_healthy: res.code == 200 - name: Backup Service Check id: backup uses: http skipif: outputs.primary.primary_healthy with: method: GET url: "{{vars.backup_url}}/health" outputs: backup_healthy: res.code == 200 - name: Report uses: hello echo: | Primary: {{outputs.primary.primary_healthy ? "Online" : "Offline"}} Backup: {{outputs.backup_healthy ?? "not checked"}}

State Management

State is what one part of a run leaves for another: a job’s status, a step’s outputs, and the references that reach across jobs.

Job State Tracking

Probe tracks comprehensive state information for each job:

# Job states available for reference: jobs: - id: example-job name: example-job steps: - name: Example Step uses: hello echo: "Job states can be referenced from other jobs" - id: dependent-job name: dependent-job needs: [example-job] steps: - name: Check Job States uses: hello echo: | Job State Information:

Step State and Output Management

Each step produces state and output information:

steps: - name: API Test Step id: api-test uses: http with: method: GET url: "{{vars.API_URL}}/test" test: res.code == 200 && (rt.sec * 1000) < 1000 outputs: response_time: (rt.sec * 1000) status_code: res.status api_healthy: res.code == 200 - name: Reference Previous Step uses: hello echo: | Previous Step Information: Step outputs: Response time: {{outputs['api-test'].response_time}}ms Status code: {{outputs['api-test'].status_code}} API healthy: {{outputs['api-test'].api_healthy}}

Cross-Job State References

Jobs can reference state from other jobs:

jobs: - id: health-check name: Health Check steps: - name: Check Service id: health-check outputs: service_healthy: true - id: performance-test name: Performance Test needs: [health-check] steps: - name: Load Test id: performance-test outputs: avg_response_time: 250 - id: reporting name: Generate Report needs: [health-check, performance-test] steps: - name: Status Report uses: hello echo: | System Status Report: Performance Test: {{ "⏸️ Skipped" }} "Average Response Time: " + outputs['performance-test'].avg_response_time + "ms" : "Performance data not available"}}

Timing and Performance

How long a run takes is decided by three things: when each job starts, how long a step may wait before it is cut off, and how much of the graph can proceed at once.

Execution Timing

Probe tracks timing information at multiple levels:

jobs: - id: timing-example name: Timing Example steps: - name: Quick Operation id: quick uses: http with: method: GET url: "{{vars.API_URL}}/ping" test: res.code == 200 outputs: ping_time: (rt.sec * 1000) - name: Slow Operation id: slow uses: http with: method: GET url: "{{vars.API_URL}}/complex-query" test: res.code == 200 outputs: query_time: (rt.sec * 1000) - name: Timing Summary uses: hello echo: | Operation Timing: Quick operation: {{outputs.quick.ping_time}}ms Slow operation: {{outputs.slow.query_time}}ms Total step time: {{outputs.quick.ping_time + outputs.slow.query_time}}ms Performance classification: Quick: {{outputs.quick.ping_time < 100 ? "Excellent" : (outputs.quick.ping_time < 500 ? "Good" : "Slow")}} Slow: {{outputs.slow.query_time < 1000 ? "Fast" : (outputs.slow.query_time < 5000 ? "Acceptable" : "Too Slow")}}

Timeout Management

Configure timeouts at different levels:

jobs: - id: timeout-management name: Timeout Management Example timeout: 300s # Job-level timeout (5 minutes) steps: - name: Quick API Call uses: http timeout: 10s # Step-level timeout with: method: GET url: "{{vars.API_URL}}/quick" test: res.code == 200 - name: Database Query uses: http timeout: 60s # Longer timeout for complex operation with: method: GET url: "{{vars.DB_API}}/complex-query" test: res.code == 200 - name: External Service Call uses: http timeout: 30s # External services may be slower with: method: GET url: "{{vars.EXTERNAL_API}}/data" test: res.code == 200

Parallel Execution Optimization

Optimize workflow execution through effective parallelization:

name: Optimized Parallel Execution description: Efficiently organize jobs for maximum parallelism jobs: # Tier 1: Independent foundation checks (all parallel) - id: database-check name: Database Health steps: - name: DB Connection Test uses: http with: method: GET url: "{{vars.DB_URL}}/ping" test: res.code == 200 - id: cache-check name: Cache Health steps: - name: Cache Connection Test uses: http with: method: GET url: "{{vars.CACHE_URL}}/ping" test: res.code == 200 - id: network-check name: Network Connectivity steps: - name: External API Test uses: http with: method: GET url: "{{vars.EXTERNAL_API}}/ping" test: res.code == 200 # Tier 2: Service-level checks (parallel, depend on infrastructure) - id: user-service-test name: User Service Test needs: [database-check, cache-check] steps: - name: User API Test uses: http with: method: GET url: "{{vars.USER_API}}/health" test: res.code == 200 - id: order-service-test name: Order Service Test needs: [database-check] steps: - name: Order API Test uses: http with: method: GET url: "{{vars.ORDER_API}}/health" test: res.code == 200 - id: notification-service-test name: Notification Service Test needs: [network-check] steps: - name: Notification API Test uses: http with: method: GET url: "{{vars.NOTIFICATION_API}}/health" test: res.code == 200 # Tier 3: Integration tests (depend on services) - id: user-order-integration name: User-Order Integration needs: [user-service-test, order-service-test] steps: - name: Integration Test uses: http with: method: GET url: "{{vars.API_URL}}/integration/user-order" test: res.code == 200 # Tier 4: Final validation (depends on integration) - id: end-to-end-test name: End-to-End Test needs: [user-order-integration, notification-service-test] steps: - name: Complete Workflow Test uses: http with: method: GET url: "{{vars.API_URL}}/e2e/complete-workflow" test: res.code == 200

Execution Visualization:

Time 0-T1: database-check, cache-check, network-check (parallel) Time T1-T2: user-service-test, order-service-test, notification-service-test (parallel) Time T2-T3: user-order-integration Time T3-T4: end-to-end-test

Error Propagation and Recovery

A failure does not stop at the step that produced it. What happens next is governed by how it propagates through the graph, and by what the workflow does to recover.

Error Propagation Model

Understanding how errors propagate through the execution model:

jobs: - id: critical-foundation name: Critical Foundation steps: - name: Critical Check uses: http with: method: GET url: "{{vars.CRITICAL_SERVICE}}/health" test: res.code == 200 # A failing step marks the job failed; later steps of the job still run - id: dependent-service name: Dependent Service needs: [critical-foundation] # Won't execute if foundation fails steps: - name: Service Test uses: http with: method: GET url: "{{vars.SERVICE_URL}}/test" test: res.code == 200 - id: resilient-check name: Resilient Check # No dependencies - always executes steps: - name: Independent Check uses: http with: method: GET url: "{{vars.INDEPENDENT_SERVICE}}/health" test: res.code == 200 - id: conditional-cleanup name: Conditional Cleanup needs: [critical-foundation, dependent-service, resilient-check] steps: - name: Cleanup Failed State uses: hello echo: | Cleaning up after failures:

Recovery Execution Model

Implement recovery workflows that execute based on failure patterns:

jobs: - id: primary-workflow name: Primary Workflow steps: - name: Main Process id: main uses: http with: method: GET url: "{{vars.API_URL}}/main-process" test: res.code == 200 outputs: process_successful: res.code == 200 - id: recovery-workflow name: Recovery Workflow steps: - name: Diagnose Failure id: diagnose uses: http with: method: GET url: "{{vars.API_URL}}/diagnostics" test: res.code == 200 outputs: diagnosis: res.body.issue_type - name: Automated Recovery uses: http with: url: "{{vars.API_URL}}/recovery/auto" method: POST test: res.code == 200 - name: Manual Recovery Alert uses: hello echo: "🚨 Critical failure detected - manual intervention required" - id: validation-workflow name: Validation Workflow needs: [primary-workflow, recovery-workflow] steps: - name: Validate Final State id: validation-workflow uses: http with: method: GET url: "{{vars.API_URL}}/validate" test: res.code == 200 outputs: system_healthy: res.code == 200 - id: final-report name: Final Report needs: [validation-workflow] steps: - name: Execution Summary uses: hello echo: | Workflow Execution Summary: Overall result: {{ "❌ System failed" }}

Resource Management

Each action runs in a process of its own, so a run has processes to start and stop.

Action Lifecycle Management

Probe starts an action for each step that uses it:

jobs: - id: multi-action-workflow name: Multi Action Workflow steps: # The http action is started for this step - name: API Test uses: http with: method: GET url: "{{vars.API_URL}}/test" test: res.code == 200 # The smtp action is started for this step - name: Send Notification uses: smtp with: addr: "{{vars.SMTP_HOST}}:25" from: "probe@example.com" to: "admin@company.com" subject: "Test Completed" session: 1 message: 1 length: 500 echo: "API test completed successfully" - name: Debug Message uses: hello with: message: "Debug checkpoint reached" # The http action is started again, in a new process - name: Follow-up API Test uses: http with: method: GET url: "{{vars.API_URL}}/follow-up" test: res.code == 200

Action lifecycle:

  1. A process is started for the action when a step uses it
  2. The step runs in it, and the process is stopped once the step has its result
  3. A later step that uses the same action starts a new process; what the action keeps in the job, such as the cookies of keep_cookies, is handed to it by Probe

Memory and Performance Optimization

Probe optimizes execution for performance and resource usage:

jobs: - id: optimized-workflow name: Performance Optimized Workflow steps: # Efficient: Direct property access - name: User Data Collection id: user-data uses: http with: method: GET url: "{{vars.API_URL}}/users" test: res.code == 200 outputs: user_count: res.body.total_users # Extract specific value first_user_id: res.body.users[0].id # Direct array access # Avoid: large_user_list: res.body.users (stores entire array) # Efficient: Conditional processing - name: Process Large Dataset uses: http with: method: GET url: "{{vars.API_URL}}/users/batch-process" test: res.code == 200 # Efficient: Scoped outputs - name: Summary Generation uses: hello echo: | Processing Summary: Total users: {{outputs['user-data'].user_count}} First user: {{outputs['user-data'].first_user_id}} # Efficient output without storing unnecessary data

Best Practices

The points below follow from the execution model: how dependencies are shaped, what happens on failure, how much is carried in outputs, and how the flow is made legible.

1. Dependency Design

Declare a dependency only where the data or the ordering actually requires it; every extra one removes parallelism.

# Good: Logical dependency grouping jobs: - id: infrastructure name: infrastructure - id: application name: application needs: [infrastructure] - id: integration name: integration needs: [application] # Avoid: Unnecessary dependencies jobs: - id: independent-check-1 name: independent-check-1 - id: independent-check-2 name: independent-check-2 needs: [independent-check-1] # Unnecessary if truly independent

2. Error Handling Strategy

Decide per step whether a failure should stop the run or be carried forward.

# Good: Strategic error handling - name: Critical Operation test: res.code == 200 - name: Optional Operation test: res.code == 200

3. Output Efficiency

Publish the values later steps read, not the whole response.

# Good: Efficient outputs outputs: essential_data: res.body.id computed_value: len(res.body.items) status_flag: res.code == 200 # Avoid: Storing large objects outputs: # entire_response: res.body # Could be very large

4. Execution Flow Documentation

The workflow’s description is where the order it expects can be stated.

name: Well-Documented Workflow description: | Execution flow: 1. Infrastructure validation (parallel) 2. Service health checks (parallel, depends on infrastructure) 3. Integration testing (sequential, depends on services) 4. Reporting (depends on all previous stages) Expected execution time: 2-5 minutes Critical path: infrastructure → services → integration → reporting

What’s Next?

Now that you understand the execution model, explore:

  1. File Merging - Learn configuration composition techniques
  2. How-tos - See practical execution patterns in action
  3. Reference - Detailed syntax and configuration reference

Understanding the execution model helps you design efficient, predictable workflows that make optimal use of parallelism and handle failures gracefully.

Updated at