rpa-patterns

omerlefaruk's avatarfrom omerlefaruk

Common RPA automation patterns (retry, polling, circuit breaker) for resilient automation. Use when: implementing RPA workflows, error handling, resilient automation, retry with backoff, polling for conditions, circuit breaker patterns.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 3, 2026

When & Why to Use This Skill

This Claude skill provides a comprehensive framework of resilient RPA (Robotic Process Automation) patterns designed to build highly reliable and fault-tolerant automated workflows. It offers production-ready implementations for exponential backoff retries, condition polling, circuit breakers, and graceful recovery strategies, ensuring that web automations can effectively navigate transient network failures, dynamic UI changes, and external service instabilities.

Use Cases

  • Handling transient network timeouts and API rate limits by implementing exponential backoff retry logic to ensure task completion.
  • Managing dynamic web content by using polling patterns to wait for specific UI elements or conditions before proceeding with automation steps.
  • Protecting automation pipelines from cascading failures using circuit breakers when external websites or services experience downtime.
  • Executing high-volume data scraping or processing tasks where non-critical errors are logged and bypassed to prevent workflow interruption.
  • Implementing automated recovery strategies, such as using alternative element selectors or fallback actions, when primary UI interactions fail.
namerpa-patterns
descriptionCommon RPA automation patterns (retry, polling, circuit breaker) for resilient automation. Use when: implementing RPA workflows, error handling, resilient automation, retry with backoff, polling for conditions, circuit breaker patterns.

RPA Patterns Skill

Resilience and retry patterns for reliable RPA automation.

Overview

RPA automation must handle unreliable external systems: network timeouts, dynamic UI loading, rate limits, and transient failures. This skill provides proven patterns for building robust automations.

Quick Reference

Pattern Use Case Example
Retry with Backoff Transient failures, network issues examples/retry-pattern.py
Polling Wait for UI element or condition examples/polling-pattern.py
Circuit Breaker Prevent cascade failures, rate limits examples/circuit-breaker-pattern.py
Continue on Error Non-critical operations, data collection Inline below
Recovery Strategy Graceful degradation, fallback actions Inline below

Built-in BrowserBaseNode Support

Browser nodes already have retry support via execute_with_retry():

from casare_rpa.nodes.browser.browser_base import BrowserBaseNode

class MyClickNode(BrowserBaseNode):
    async def execute(self, context):
        page = await self.get_page(context)

        # Built-in retry (uses retry_count, retry_interval from config)
        result, attempts = await self.execute_with_retry(
            lambda: page.click("#submit-btn"),
            operation_name="click submit"
        )

Config properties (auto-generated in visual nodes):

  • retry_count: int = 0 (number of retries after initial failure)
  • retry_interval: int = 1000 (ms between retries)

Pattern 1: Exponential Backoff Retry

For operations without built-in retry support.

import asyncio
from loguru import logger

async def retry_with_backoff(
    operation: Callable[[], Awaitable[T]],
    max_attempts: int = 3,
    base_delay_ms: int = 1000,
    max_delay_ms: int = 10000,
) -> T:
    """Retry with exponential backoff: 1s, 2s, 4s, 8s..."""
    for attempt in range(1, max_attempts + 1):
        try:
            return await operation()
        except Exception as e:
            if attempt == max_attempts:
                raise

            delay = min(base_delay_ms * (2 ** (attempt - 1)), max_delay_ms)
            logger.warning(f"Attempt {attempt}/{max_attempts} failed: {e}. Retrying in {delay}ms")
            await asyncio.sleep(delay / 1000)

Pattern 2: Polling for Condition

Wait for dynamic UI elements or async operations.

async def poll_for_condition(
    condition: Callable[[], Awaitable[bool]],
    timeout_ms: int = 30000,
    interval_ms: int = 500,
) -> bool:
    """Poll until condition is true or timeout."""
    deadline = asyncio.get_event_loop().time() + (timeout_ms / 1000)

    while asyncio.get_event_loop().time() < deadline:
        if await condition():
            return True
        await asyncio.sleep(interval_ms / 1000)

    raise TimeoutError(f"Condition not met within {timeout_ms}ms")

Usage with Playwright:

# Instead of hardcoded sleep
await asyncio.sleep(5)  # BAD

# Use polling
async def is_loaded(page):
    return await page.locator(".data-loaded").count() > 0

await poll_for_condition(lambda: is_loaded(page))  # GOOD

Pattern 3: Circuit Breaker

Prevent hammering failing services and enable auto-recovery.

from dataclasses import dataclass
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"      # Normal operation
    OPEN = "open"          # Failing, reject requests
    HALF_OPEN = "half_open"  # Testing recovery

@dataclass
class CircuitBreaker:
    failure_threshold: int = 5
    timeout_ms: int = 60000
    half_open_attempts: int = 1

    def __post_init__(self):
        self._state = CircuitState.CLOSED
        self._failures = 0
        self._last_failure_time = 0

    async def call(self, operation: Callable[[], Awaitable[T]]) -> T:
        """Execute operation with circuit breaker protection."""
        if self._state == CircuitState.OPEN:
            if self._should_attempt_reset():
                self._state = CircuitState.HALF_OPEN
            else:
                raise CircuitBreakerOpenError("Circuit is OPEN")

        try:
            result = await operation()
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise

Pattern 4: Continue on Error

For non-critical operations where failure should not stop workflow.

@properties(
    PropertyDef("on_error", PropertyType.CHOICE, default="fail",
                options=["fail", "continue", "retry"]),
)
@node(category="data")
class DataCollectNode(BaseNode):
    async def execute(self, context):
        on_error = self.get_parameter("on_error")
        results = []

        for item in items:
            try:
                result = await self.process_item(item)
                results.append(result)
            except Exception as e:
                logger.error(f"Failed to process {item}: {e}")

                if on_error == "fail":
                    raise
                elif on_error == "continue":
                    results.append({"error": str(e)})
                elif on_error == "retry":
                    # Retry logic here
                    pass

        self.set_output_value("results", results)

Pattern 5: Recovery Strategy

Fallback actions when primary operation fails.

async def execute_with_fallback(
    primary: Callable[[], Awaitable[T]],
    fallback: Callable[[], Awaitable[T]],
    fallback_reason: str = "primary failed",
) -> T:
    """Try primary, use fallback on failure."""
    try:
        return await primary()
    except Exception as e:
        logger.warning(f"Primary operation failed: {e}. Using fallback: {fallback_reason}")
        return await fallback()

RPA-Specific Error Handling

Screenshot on Failure

Already built into BrowserBaseNode:

# In node config, enable:
# screenshot_on_fail: True
# screenshot_on_fail: "./screenshots"

# Or manually:
await self.screenshot_on_failure(page, prefix="click_failed")

Element Not Found Recovery

async def click_with_recovery(page, selector):
    """Click with multiple selector strategies."""
    try:
        await page.click(selector)
    except Error:
        # Try alternative selectors
        for alt in get_alternative_selectors(selector):
            try:
                await page.click(alt)
                return
            except Error:
                continue
        raise

Dynamic Wait Strategies

async def smart_wait(page, selector, timeout_ms=30000):
    """Wait with progressive timeouts."""
    # Fast check first
    try:
        if await page.locator(selector).count() > 0:
            return
    except Error:
        pass

    # Then full wait
    await page.wait_for_selector(selector, timeout=timeout_ms)

Examples

See examples/ folder for complete implementations:

File Description
retry-pattern.py Exponential backoff with jitter
polling-pattern.py Condition polling with timeout
circuit-breaker-pattern.py Full circuit breaker with metrics

Cross-References

Topic Location
BrowserBaseNode retry src/casare_rpa/nodes/browser/browser_base.py
Node error handling .brain/rules/error-handling.md
Control flow nodes src/casare_rpa/nodes/control_flow/

Parent: ../_index.md Last updated: 2025-12-26