AltScore
Workflow Builder/Integration Guide

Execution and Webhooks

The Workflow Builder V2 execution engine provides reliable, scalable workflow execution with durability guarantees. This guide covers the execution lifecycle, integration patterns, and operational considerations for running workflows in production.

Execution Lifecycle

Every workflow execution progresses through a defined set of states.

Execution States

StateDescription
PENDINGExecution created, waiting to start
RUNNINGWorkflow actively executing tasks
COMPLETEDWorkflow finished successfully
FAILEDExecution failed with an error
CANCELLEDExecution was manually cancelled

State Transitions

PENDING ──────> RUNNING ──────> COMPLETED
                   |
                   |──────────> FAILED
                   |
                   └──────────> CANCELLED

PENDING to RUNNING

The execution transitions to RUNNING when:

  • The system schedules the workflow
  • A processing slot becomes available
  • The start task begins processing

RUNNING to COMPLETED

The execution completes successfully when:

  • All tasks in the execution path finish without errors
  • The end task produces the final output
  • No unhandled exceptions occur

RUNNING to FAILED

The execution fails when:

  • A task throws an unhandled exception
  • Retry attempts are exhausted
  • A timeout is exceeded
  • An exception task is triggered

RUNNING to CANCELLED

The execution is cancelled when:

  • A user manually cancels via the UI or API
  • A parent workflow cancels a child workflow
  • System-level cancellation is triggered

Monitoring Execution State

You can monitor execution state through:

  1. UI Dashboard: Real-time status updates with visual indicators
  2. API Polling: Query /v1/executions/{id} for current status
  3. Webhooks: Configure callbacks for state transitions

Polling Pattern with Retry Logic

For asynchronous executions, implement polling with exponential backoff:

import time
from typing import Optional
from altscore import AltScore
 
def poll_execution_status(
    alt_client: AltScore,
    execution_id: str,
    max_wait_seconds: int = 300,
    initial_interval: float = 1.0,
    max_interval: float = 30.0,
    backoff_multiplier: float = 1.5
) -> dict:
    """
    Poll for execution completion with exponential backoff.
 
    Args:
        alt_client: Initialized AltScore client
        execution_id: The execution ID to poll
        max_wait_seconds: Maximum time to wait for completion
        initial_interval: Starting poll interval in seconds
        max_interval: Maximum poll interval in seconds
        backoff_multiplier: Factor to increase interval each poll
 
    Returns:
        The completed execution result
 
    Raises:
        TimeoutError: If execution doesn't complete in time
        Exception: If execution fails
    """
    start_time = time.time()
    interval = initial_interval
 
    while time.time() - start_time < max_wait_seconds:
        # Get current status
        execution = alt_client.borrower_central.executions.retrieve(execution_id)
 
        if execution.status == "completed":
            return execution.output
 
        if execution.status == "failed":
            raise Exception(
                f"Execution failed: {execution.error_message}"
            )
 
        if execution.status == "cancelled":
            raise Exception("Execution was cancelled")
 
        # Wait before next poll with exponential backoff
        time.sleep(interval)
        interval = min(interval * backoff_multiplier, max_interval)
 
    raise TimeoutError(
        f"Execution {execution_id} did not complete within {max_wait_seconds}s"
    )
 
 
# Usage example
alt_client = AltScore(
    client_id="your-client-id",
    client_secret="your-client-secret",
    environment="production"
)
 
# Start async execution
result = alt_client.borrower_central.workflows.execute(
    alias="credit-decision",
    version="v1",
    input_data={"borrower_id": "brw_123"},
    sync=False  # Async execution
)
 
# Poll for completion
try:
    output = poll_execution_status(
        alt_client,
        result.execution_id,
        max_wait_seconds=120
    )
    print(f"Decision: {output['decision']}")
except TimeoutError:
    print("Execution taking too long, consider checking manually")
except Exception as e:
    print(f"Execution failed: {e}")

Webhook Integration

Configure webhooks to receive real-time notifications when executions complete, eliminating the need for polling.

The quickest way to set them up is from the AltScore hub, under Settings > Webhooks: there you register your endpoint, subscribe it to the bc.execution.completed event, get the signing secret and browse the delivery history. The Webhooks guide covers signature verification and retries in detail.

Webhook Payload Structure

When an execution completes, AltScore sends a POST request to your configured endpoint. The execution data travels in data.entity:

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "event": "bc.execution.completed",
  "timestamp": "2026-02-10T18:45:32Z",
  "entityType": "bc.execution",
  "version": "20240901",
  "data": {
    "entity": {
      "executionId": "exec_01HQ3K5X7Y8Z9ABCDEFGHIJK",
      "workflowAlias": "credit-decision",
      "status": "completed",
      "isSuccess": true
    }
  },
  "actor": {
    "id": "user-135",
    "actorType": "system"
  }
}

data.entity is the execution object. The event envelope and the rest of the available events are described in Events.

Webhook Handler Example

Every delivery is signed with the svix-id, svix-timestamp and svix-signature headers. Verify the signature against the raw body before processing anything: without it, anyone who knows the URL can send you a forged execution. You get each endpoint's secret from Settings > Webhooks.

from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
 
app = FastAPI()
WEBHOOK_SECRET = "whsec_..."  # Settings > Webhooks
 
@app.post("/webhooks/altscore")
async def handle_altscore_webhook(request: Request):
    payload = await request.body()
    try:
        event = Webhook(WEBHOOK_SECRET).verify(payload, dict(request.headers))
    except WebhookVerificationError:
        raise HTTPException(status_code=401, detail="Invalid signature")
 
    if event["event"] == "bc.execution.completed":
        execution = event["data"]["entity"]
 
        if execution["isSuccess"]:
            await process_credit_decision(
                execution_id=execution["executionId"],
                status=execution["status"],
            )
        else:
            await handle_execution_failure(execution_id=execution["executionId"])
 
    return {"status": "received"}
 
async def process_credit_decision(execution_id: str, status: str):
    # Your business logic here
    pass
 
async def handle_execution_failure(execution_id: str):
    # Your error handling logic here
    pass

Pass verify the raw body, unparsed and unserialized: the signature is computed over the exact bytes you received. And make your handler idempotent, keyed on svix-id: a retry can duplicate an event you already processed.

Registering a Webhook

Besides the hub, you can register it via API:

curl --request POST \
  --url https://bc.altscore.ai/v1/webhooks/endpoints \
  --header 'Authorization: Bearer {token}' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Execution Completion Handler",
    "url": "https://your-api.com/webhooks/altscore",
    "events": ["bc.execution.completed"]
  }'

The full reference is in Webhooks API.

Webhooks are the recommended approach for production integrations. They eliminate polling overhead and provide real-time notifications. Use polling only for simple integrations or when webhooks are not feasible. If a delivery fails, Svix retries it automatically and you can retry it by hand from Settings > Webhooks.

Synchronous vs Asynchronous Execution

Workflow Builder supports both synchronous and asynchronous execution modes to accommodate different use cases.

Synchronous Execution

In synchronous mode, the API request waits for the workflow to complete before returning.

Request

POST /v1/workflows/{alias}/execute
Content-Type: application/json
 
{
  "input": {
    "borrower_id": "12345"
  },
  "sync": true
}

Response (returned after workflow completes)

{
  "execution_id": "exec-abc123",
  "status": "COMPLETED",
  "output": {
    "decision": "approved",
    "score": 750
  }
}

When to use synchronous execution:

  • Real-time decisions needed in user-facing flows
  • Workflows that complete in under 30 seconds
  • Integration with systems expecting immediate responses

Synchronous execution has a default timeout of 30 seconds. For workflows that may take longer, use asynchronous execution with polling.

Asynchronous Execution

In asynchronous mode, the API returns immediately with an execution ID. Poll for results or configure webhooks.

Request

POST /v1/workflows/{alias}/execute
Content-Type: application/json
 
{
  "input": {
    "borrower_id": "12345"
  },
  "sync": false
}

Response (returned immediately)

{
  "execution_id": "exec-abc123",
  "status": "PENDING"
}

Polling for results

GET /v1/executions/exec-abc123
{
  "execution_id": "exec-abc123",
  "status": "COMPLETED",
  "output": {
    "decision": "approved",
    "score": 750
  }
}

When to use asynchronous execution:

  • Workflows involving slow external APIs
  • Batch processing scenarios
  • Background processing where immediate response is not needed
  • Workflows with wait tasks

Batch Execution

Batch execution allows processing multiple items through the same workflow, with built-in orchestration for pre-processing, execution, and post-processing.

Batch Lifecycle

+------------------+     +------------------+     +------------------+
|  pre_processing  | --> |    processing    | --> | post_processing  |
+------------------+     +------------------+     +------------------+
        |                        |                        |
        v                        v                        v
   Validate input         Execute workflow          Generate output
   Parse file             for each item             files/reports
   Transform data

Batch States

StateDescription
pendingBatch created, waiting to start
pre_processingValidating and preparing input data
processingExecuting workflows for each item
post_processingGenerating output files and reports
completeBatch finished
failedBatch failed during any phase

Creating a Batch

POST /v1/workflows/{alias}/batch
Content-Type: application/json
 
{
  "items": [
    {"borrower_id": "12345", "amount": 10000},
    {"borrower_id": "67890", "amount": 15000}
  ],
  "options": {
    "parallelism": 10,
    "stopOnError": false
  }
}

Response

{
  "batch_id": "batch-xyz789",
  "status": "pending",
  "total_items": 2
}

Batch Configuration Options

OptionTypeDescription
parallelismnumberMax concurrent workflow executions (default: 10)
stopOnErrorbooleanStop batch on first error (default: false)
notifyOnCompletebooleanSend webhook when batch completes
outputFormatstringFormat for output files: "json", "csv"

Pre-Processing Phase

During pre-processing, the system:

  1. Validates input data: Ensures all items have required fields
  2. Parses input files: Handles CSV, JSON, or other file formats
  3. Transforms data: Applies any configured transformations
  4. Creates execution records: Prepares individual executions for each item

If pre-processing fails (e.g., invalid data format), the batch moves to failed state without executing any workflows.

Processing Phase

During processing:

  1. Items are processed according to parallelism settings
  2. Each item triggers a separate workflow execution
  3. Individual failures don't stop other items (unless stopOnError is true)
  4. Progress is tracked and available via API

Monitoring batch progress

GET /v1/execution-batches/{batch_id}
{
  "batch_id": "batch-xyz789",
  "status": "processing",
  "total_items": 100,
  "completed": 45,
  "failed": 2,
  "pending": 53
}

Post-Processing Phase

After all items are processed:

  1. Generate output files: Compile results into downloadable files
  2. Create summary reports: Aggregate statistics and outcomes
  3. Trigger webhooks: Notify external systems of completion
  4. Clean up resources: Archive temporary data

Retry Policies

The execution engine supports configurable retry policies at both the workflow and task level.

Default Retry Behavior

ConfigurationDefault Value
Max attempts3
Initial interval1 second
Backoff coefficient2.0
Max interval60 seconds

Task-Specific Retry Policies

Different task types have tailored retry behaviors:

HTTP Tasks

max_attempts: 3
initial_interval: 2s
backoff: exponential
retry_on:
  - timeout
  - 5xx errors
  - connection errors
do_not_retry:
  - 4xx errors (except 429)

Python Tasks

max_attempts: 1
retry_on:
  - infrastructure errors
do_not_retry:
  - code exceptions

External Integration Tasks

max_attempts: 5
initial_interval: 5s
backoff: exponential
max_interval: 120s

Configuring Custom Retry Policies

In the Advanced tab of task configuration:

SettingDescription
Max RetriesMaximum number of retry attempts
Initial DelayTime before first retry (seconds)
Max DelayMaximum time between retries (seconds)
Backoff MultiplierFactor to increase delay between retries

Be cautious with retry policies for tasks that modify external state. Ensure operations are idempotent or use appropriate safeguards to prevent duplicate processing.

Retry vs. Workflow Restart

Understanding when retries occur vs. when a full restart is needed:

ScenarioBehavior
Transient network errorAutomatic retry at task level
Task timeoutRetry up to max attempts
Invalid input dataFail without retry
External service downRetry with exponential backoff
Code exception in PythonFail without retry
Workflow timeoutRequires manual restart

Error Handling

The execution engine provides comprehensive error handling capabilities.

Error Capture

When a task fails, the engine captures:

  • Error message: Human-readable description
  • Error code: Machine-readable identifier
  • Stack trace: Full execution trace (for debugging)
  • Task context: Input data and configuration at time of failure
  • Execution history: All events leading to the failure

Error Categories

CategoryDescriptionTypical Response
ValidationInput data doesn't meet requirementsFail fast, provide clear message
ExternalThird-party service unavailableRetry with backoff
TimeoutOperation exceeded time limitRetry or fail based on config
AuthorizationPermission deniedFail, log security event
Business LogicPolicy or rule violationFail, capture decision context
InfrastructureSystem-level failureAutomatic recovery

Exception Handling in Workflows

Use the exception task to implement explicit error handling:

[Start] --> [Validate Input] --> [Conditional]
                                      |
                    +-----------------+-----------------+
                    |                                   |
                    v                                   v
            [Valid: Continue]              [Invalid: Exception Task]
                    |                                   |
                    v                                   v
            [Process Data]                    Workflow Fails with
                    |                         "Invalid input" error
                    v
                  [End]

Viewing Error Details in UI

Failed tasks display error information:

  1. Node indicator: Red border and error icon on failed tasks
  2. Properties panel: Full error details in the Logs tab
  3. Execution history: Timeline view showing where failure occurred
  4. Error context: Input values and configuration at failure point

Programmatic Error Handling

When calling the execution API:

const response = await fetch('/v1/workflows/my-workflow/execute', {
  method: 'POST',
  body: JSON.stringify({ input: data })
})
 
const result = await response.json()
 
if (result.status === 'FAILED') {
  // Access error details
  const error = result.error
  console.error(`Workflow failed: ${error.message}`)
  console.error(`Error code: ${error.code}`)
  console.error(`Failed task: ${error.taskAlias}`)
}

Error Recovery Strategies

StrategyDescriptionWhen to Use
Automatic retryEngine retries failed tasksTransient failures
Manual retryUser triggers retry via UI/APIAfter fixing root cause
Skip and continueMark task as skipped, continue workflowNon-critical tasks
Compensating actionExecute cleanup/rollback logicPartial failures
EscalationCreate alert for human reviewComplex failures

Implementing Error Recovery in Code

from altscore import AltScore
from typing import Optional
import logging
 
logger = logging.getLogger(__name__)
 
class WorkflowExecutionManager:
    """Manages workflow execution with error recovery."""
 
    def __init__(self, alt_client: AltScore):
        self.client = alt_client
 
    def execute_with_retry(
        self,
        alias: str,
        version: str,
        input_data: dict,
        max_retries: int = 3,
        retry_on_failure: bool = True
    ) -> dict:
        """
        Execute workflow with automatic retry on transient failures.
 
        Args:
            alias: Workflow alias
            version: Workflow version
            input_data: Workflow input
            max_retries: Maximum retry attempts
            retry_on_failure: Whether to retry failed executions
 
        Returns:
            Execution output on success
        """
        last_error = None
 
        for attempt in range(max_retries):
            try:
                # Execute workflow
                result = self.client.borrower_central.workflows.execute(
                    alias=alias,
                    version=version,
                    input_data=input_data,
                    sync=True
                )
 
                if result.is_success:
                    return result.output
 
                # Check if error is retryable
                if not retry_on_failure or not self._is_retryable_error(result):
                    raise ExecutionError(
                        f"Workflow failed: {result.error_message}",
                        execution_id=result.execution_id,
                        error_code=result.status_code
                    )
 
                last_error = result.error_message
                logger.warning(
                    f"Attempt {attempt + 1} failed: {last_error}, retrying..."
                )
 
            except ConnectionError as e:
                last_error = str(e)
                logger.warning(
                    f"Connection error on attempt {attempt + 1}: {e}"
                )
 
        raise ExecutionError(
            f"Workflow failed after {max_retries} attempts: {last_error}"
        )
 
    def _is_retryable_error(self, result) -> bool:
        """Determine if an error should trigger a retry."""
        retryable_codes = [
            "EXTERNAL_SERVICE_ERROR",
            "TIMEOUT",
            "CONNECTION_ERROR",
            "RATE_LIMITED"
        ]
        return result.error_code in retryable_codes
 
    def recover_failed_batch(
        self,
        batch_id: str,
        retry_mode: str = "failed_items"
    ) -> str:
        """
        Recover a failed batch by retrying failed items.
 
        Args:
            batch_id: The batch execution ID
            retry_mode: One of 'failed_items', 'unsuccessful_sources_items',
                       'failed_and_unsuccessful_sources_items'
 
        Returns:
            New batch ID for the retry
        """
        # Get batch status
        batch = self.client.borrower_central.execution_batches.retrieve(batch_id)
 
        if batch.status not in ["complete", "post_processing_failed"]:
            raise ValueError(
                f"Cannot recover batch in status: {batch.status}"
            )
 
        summary = batch.state.batch_items_executions_summary
 
        if retry_mode == "failed_items" and summary.failed == 0:
            logger.info("No failed items to retry")
            return batch_id
 
        # Retry failed items
        retry_result = self.client.borrower_central.execution_batches.retry(
            batch_id=batch_id,
            retry_mode=retry_mode,
            use_previous_inputs=True
        )
 
        logger.info(
            f"Batch retry started: {retry_result.execution_batch_id}"
        )
 
        return retry_result.execution_batch_id
 
 
class ExecutionError(Exception):
    """Custom exception for workflow execution failures."""
 
    def __init__(
        self,
        message: str,
        execution_id: Optional[str] = None,
        error_code: Optional[str] = None
    ):
        super().__init__(message)
        self.execution_id = execution_id
        self.error_code = error_code
 
 
# Usage example
alt_client = AltScore(
    client_id="your-client-id",
    client_secret="your-client-secret",
    environment="production"
)
 
manager = WorkflowExecutionManager(alt_client)
 
try:
    output = manager.execute_with_retry(
        alias="credit-decision",
        version="v1",
        input_data={"borrower_id": "brw_123"},
        max_retries=3
    )
    print(f"Decision: {output['decision']}")
except ExecutionError as e:
    print(f"Execution failed: {e}")
    if e.execution_id:
        print(f"Check execution: {e.execution_id}")

Performance Considerations

Execution Throughput

  • The system can process hundreds of concurrent workflow executions
  • Task parallelism within a workflow is limited by dependencies
  • Batch processing scales horizontally to handle increased load

Latency Optimization

  1. Minimize external calls: Batch data retrieval where possible
  2. Use caching: Leverage data source caching for repeated lookups
  3. Optimize task order: Place fast-failing validations early
  4. Configure appropriate timeouts: Avoid waiting for unresponsive services

Monitoring and Alerting

Track these metrics for healthy execution:

MetricDescriptionAlert Threshold
Execution durationTime from start to completion> 2x average
Task failure ratePercentage of failed tasks> 5%
Queue depthPending executions waiting> 100
System utilizationProcessing capacity usage> 80%

Best Practices

Designing for Reliability

  1. Make tasks idempotent: Safe to retry without side effects
  2. Handle partial failures: Use compensating transactions when needed
  3. Set appropriate timeouts: Balance between reliability and responsiveness
  4. Log key decisions: Include context for debugging and auditing

Production Deployment

  1. Use async execution: Avoid blocking on long-running workflows
  2. Monitor execution queues: Alert on growing backlogs
  3. Configure webhooks: Integrate with monitoring systems
  4. Plan for failure: Implement alerting and escalation procedures

Testing Workflows

  1. Use test mode: Execute with sample data before publishing
  2. Verify error handling: Test failure scenarios explicitly
  3. Monitor execution logs: Review task-level output during testing
  4. Test at scale: Validate batch processing with realistic volumes