Chapter 19Lesson 02210–270 min

RPA Tasks, Long-Running Automation, and Human-in-the-Loop Workflows: Guided Hands-On Workflow

Build and resume a disposable local Robot Framework task workflow with synthetic inbox/outbox files, durable checkpoints, approval records, deterministic idempotency, and interruption evidence.

Local inbox/outboxCheckpoint markersBounded approval waitResume after interruptionOperator evidence

Learning objectives

  • Build a disposable inbox/outbox workflow whose durable state survives separate Robot processes.
  • Make each work item idempotent using stable IDs, deterministic outputs, and completion markers.
  • Simulate a human approval with an explicit record and bounded wait rather than an unattended dialog.
  • Inject an interruption after one durable checkpoint, preserve evidence, and resume without duplicate effects.
  • Separate workflow cleanup from ordinary task teardown and prove deletion stays inside the owned temporary root.

Current compatibility baseline — verified 2026-08-31. Robot Framework 7.4.2 is the stable course baseline and requires Python 3.8+. Task files use *** Tasks ***; task execution uses the same engine as tests, while generic automation/RPA mode changes terminology and enables task-oriented settings and selectors such as Task Setup, Task Teardown, Task Timeout, --task, and --rpa. The standard Dialogs 7.4.2 library is interactive and blocks while waiting for a desktop user, so it is optional only. The mandatory learning path is fully local and uses Robot Framework core plus OperatingSystem, String, DateTime, Collections, and BuiltIn. The external RPA Framework 33.0.1 release (2026-08-23; Python 3.10–3.13) is mentioned only as an optional ecosystem layer; no commercial Control Room, cloud orchestrator, email account, finance system, browser, database, SSH service, Pabot, container runtime, CI provider, or production target is required.

1. Scenario: two synthetic work items, one durable root

rf19-rpa-lab/
├── resources/
│   └── workflow.resource
├── tasks/
│   ├── workflow.robot
│   └── cleanup.robot
└── evidence/
    ├── first/
    ├── resume/
    ├── idempotent/
    └── cleanup/

system temp / rf19-rpa-demo-001/
├── .owner
├── inbox/
├── outbox/
├── approvals/
└── state/
    ├── pending/
    ├── approved/
    ├── completed/
    └── ledger.txt

The repository contains only automation source and result directories. Durable work state is intentionally outside the source tree, under the system temporary directory. The stable RUN_ID is both an ownership key and the path component used by separate Robot processes to reopen the same workflow.

2. Preflight and installation

python -m venv .venv
# Activate .venv using your shell's normal command.
python -m pip install "robotframework==7.4.2"
python --version
python -m robot --version
python -c "import tempfile; print('durable lab parent:', tempfile.gettempdir())"

# Optional only; not needed for the mandatory workflow:
# python -m pip install "rpaframework==33.0.1"

Do not install Dialogs or RPA Framework just to complete the lab: Dialogs is already a Robot standard library, and neither it nor external RPA packages are necessary for a file-based approval/checkpoint exercise.

3. Domain workflow resource: ownership, checkpoints, and idempotency

*** Settings ***
Library    OperatingSystem
Library    String
Library    DateTime
Library    Collections

*** Variables ***
${RUN_ID}              demo-001
${INTERRUPT_AFTER}     NONE
${ROOT_PREFIX}         rf19-rpa-
@{WORK_ITEMS}          WI-001    WI-002

*** Keywords ***
Resolve Workflow Paths
    ${root}=    Normalize Path    ${TEMPDIR}${/}${ROOT_PREFIX}${RUN_ID}
    VAR    ${WORK_ROOT}       ${root}                              scope=SUITE
    VAR    ${INBOX_DIR}       ${WORK_ROOT}${/}inbox                scope=SUITE
    VAR    ${OUTBOX_DIR}      ${WORK_ROOT}${/}outbox               scope=SUITE
    VAR    ${APPROVAL_DIR}    ${WORK_ROOT}${/}approvals            scope=SUITE
    VAR    ${STATE_DIR}       ${WORK_ROOT}${/}state                scope=SUITE
    VAR    ${PENDING_DIR}     ${STATE_DIR}${/}pending              scope=SUITE
    VAR    ${APPROVED_DIR}    ${STATE_DIR}${/}approved             scope=SUITE
    VAR    ${COMPLETED_DIR}   ${STATE_DIR}${/}completed            scope=SUITE
    VAR    ${LEDGER_DIR}      ${STATE_DIR}${/}ledger               scope=SUITE
    VAR    ${OWNER_FILE}      ${WORK_ROOT}${/}.owner               scope=SUITE

Validate Run Identity And Root
    Should Match Regexp    ${RUN_ID}    ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$
    ${actual}=    Normalize Path    ${WORK_ROOT}    case_normalize=${True}
    ${expected}=    Normalize Path    ${TEMPDIR}${/}${ROOT_PREFIX}${RUN_ID}    case_normalize=${True}
    Should Be Equal    ${actual}    ${expected}
    ${temp}=    Normalize Path    ${TEMPDIR}    case_normalize=${True}
    Should Start With    ${actual}    ${temp}${/}

Initialize Or Resume Workflow
    Resolve Workflow Paths
    Validate Run Identity And Root
    ${exists}=    Run Keyword And Return Status    Directory Should Exist    ${WORK_ROOT}
    IF    not $exists
        Create Directory    ${INBOX_DIR}
        Create Directory    ${OUTBOX_DIR}
        Create Directory    ${APPROVAL_DIR}
        Create Directory    ${PENDING_DIR}
        Create Directory    ${APPROVED_DIR}
        Create Directory    ${COMPLETED_DIR}
        Create Directory    ${LEDGER_DIR}
        Create File    ${OWNER_FILE}    ${RUN_ID}${\n}    encoding=UTF-8
        Create File    ${INBOX_DIR}${/}WI-001.txt    id=WI-001${\n}payload=alpha${\n}    encoding=UTF-8
        Create File    ${INBOX_DIR}${/}WI-002.txt    id=WI-002${\n}payload=beta${\n}     encoding=UTF-8
        Create File    ${PENDING_DIR}${/}WI-001.state    pending${\n}    encoding=UTF-8
        Create File    ${PENDING_DIR}${/}WI-002.state    pending${\n}    encoding=UTF-8
        Create File    ${APPROVAL_DIR}${/}WI-001.approved
        ...    decision=APPROVE${\n}operator=training-user${\n}
        ...    encoding=UTF-8
    ELSE
        Verify Owned Existing Workflow State
    END

Verify Owned Existing Workflow State
    Resolve Workflow Paths
    Validate Run Identity And Root
    Directory Should Exist    ${WORK_ROOT}
    Directory Should Exist    ${INBOX_DIR}
    Directory Should Exist    ${OUTBOX_DIR}
    Directory Should Exist    ${APPROVAL_DIR}
    Directory Should Exist    ${PENDING_DIR}
    Directory Should Exist    ${APPROVED_DIR}
    Directory Should Exist    ${COMPLETED_DIR}
    Directory Should Exist    ${LEDGER_DIR}
    File Should Exist    ${OWNER_FILE}
    File Should Exist    ${INBOX_DIR}${/}WI-001.txt
    File Should Exist    ${INBOX_DIR}${/}WI-002.txt
    ${owner}=    Get File    ${OWNER_FILE}    encoding=UTF-8
    ${owner}=    Strip String    ${owner}
    Should Be Equal    ${owner}    ${RUN_ID}
    ...    msg=Run ID does not own this workflow root: ${WORK_ROOT}

Process Batch Idempotently
    FOR    ${item_id}    IN    @{WORK_ITEMS}
        Process One Work Item    ${item_id}
        IF    $INTERRUPT_AFTER == $item_id
            Fatal Error    Injected interruption after durable checkpoint for ${item_id}
        END
    END

Process One Work Item
    [Arguments]    ${item_id}
    ${input}=    Set Variable    ${INBOX_DIR}${/}${item_id}.txt
    ${approval}=    Set Variable    ${APPROVAL_DIR}${/}${item_id}.approved
    ${pending}=    Set Variable    ${PENDING_DIR}${/}${item_id}.state
    ${approved}=    Set Variable    ${APPROVED_DIR}${/}${item_id}.state
    ${output}=    Set Variable    ${OUTBOX_DIR}${/}${item_id}.txt
    ${ledger_entry}=    Set Variable    ${LEDGER_DIR}${/}${item_id}.entry
    ${done}=    Set Variable    ${COMPLETED_DIR}${/}${item_id}.done

    ${already_done}=    Run Keyword And Return Status    File Should Exist    ${done}
    IF    $already_done
        File Should Exist    ${output}
        File Should Exist    ${ledger_entry}
        ${pending_exists}=    Run Keyword And Return Status    File Should Exist    ${pending}
        IF    $pending_exists
            Remove File    ${pending}
        END
        ${approved_exists}=    Run Keyword And Return Status    File Should Exist    ${approved}
        IF    $approved_exists
            Remove File    ${approved}
        END
        Log    ${item_id}: durable completion marker exists; safe no-op on resume.
        RETURN
    END

    File Should Exist    ${input}
    Wait Until Keyword Succeeds    5 seconds    250 milliseconds
    ...    File Should Exist    ${approval}
    ${decision}=    Get File    ${approval}    encoding=UTF-8
    ${decision_line}=    Get Lines Containing String    ${decision}    decision=
    ${decision_line}=    Strip String    ${decision_line}
    Should Be Equal    ${decision_line}    decision=APPROVE

    ${pending_exists}=    Run Keyword And Return Status    File Should Exist    ${pending}
    IF    $pending_exists
        Move File    ${pending}    ${approved}
    ELSE
        File Should Exist    ${approved}
    END

    ${source}=    Get File    ${input}    encoding=UTF-8
    ${stamp}=    Get Current Date    time_zone=UTC    result_format=%Y-%m-%dT%H:%M:%S.%fZ
    ${body}=    Catenate    SEPARATOR=${\n}    ${source}    status=completed    completed_at=${stamp}
    Create File    ${output}    ${body}${\n}    encoding=UTF-8
    ${verified}=    Get File    ${output}    encoding=UTF-8
    Should Contain    ${verified}    id=${item_id}
    Should Contain    ${verified}    status=completed

    Create File    ${ledger_entry}    ${item_id}|completed|${stamp}${\n}    encoding=UTF-8
    Create File    ${done}    id=${item_id}${\n}completed_at=${stamp}${\n}    encoding=UTF-8
    ${approved_exists}=    Run Keyword And Return Status    File Should Exist    ${approved}
    IF    $approved_exists
        Remove File    ${approved}
    END

Verify All Items Completed
    FOR    ${item_id}    IN    @{WORK_ITEMS}
        File Should Exist    ${COMPLETED_DIR}${/}${item_id}.done
        File Should Exist    ${OUTBOX_DIR}${/}${item_id}.txt
        File Should Exist    ${LEDGER_DIR}${/}${item_id}.entry
    END
    ${pending}=    List Directory    ${PENDING_DIR}
    ${approved}=    List Directory    ${APPROVED_DIR}
    Should Be Empty    ${pending}
    Should Be Empty    ${approved}

Preserve Workflow Evidence
    ${ledger_1}=    Get File    ${LEDGER_DIR}${/}WI-001.entry    encoding=UTF-8
    ${ledger_1}=    Strip String    ${ledger_1}
    ${ledger_2}=    Get File    ${LEDGER_DIR}${/}WI-002.entry    encoding=UTF-8
    ${ledger_2}=    Strip String    ${ledger_2}
    ${ledger}=    Catenate    SEPARATOR=${\n}    id|state|timestamp    ${ledger_1}    ${ledger_2}
    Create File    ${OUTPUT DIR}${/}workflow-ledger.txt    ${ledger}${\n}    encoding=UTF-8
    ${inbox}=       List Directory    ${INBOX_DIR}
    ${outbox}=      List Directory    ${OUTBOX_DIR}
    ${approvals}=   List Directory    ${APPROVAL_DIR}
    ${pending}=     List Directory    ${PENDING_DIR}
    ${approved}=    List Directory    ${APPROVED_DIR}
    ${completed}=   List Directory    ${COMPLETED_DIR}
    ${tree_text}=    Catenate    SEPARATOR=${\n}
    ...    inbox=${inbox}
    ...    outbox=${outbox}
    ...    approvals=${approvals}
    ...    pending=${pending}
    ...    approved=${approved}
    ...    completed=${completed}
    Create File    ${OUTPUT DIR}${/}workflow-tree.txt    ${tree_text}${\n}    encoding=UTF-8

Guarded Cleanup Workflow Root
    Verify Owned Existing Workflow State
    Verify All Items Completed
    ${actual}=    Normalize Path    ${WORK_ROOT}    case_normalize=${True}
    ${expected}=    Normalize Path    ${TEMPDIR}${/}${ROOT_PREFIX}${RUN_ID}    case_normalize=${True}
    Should Be Equal    ${actual}    ${expected}
    ${temp}=    Normalize Path    ${TEMPDIR}    case_normalize=${True}
    Should Start With    ${actual}    ${temp}${/}
    Preserve Workflow Evidence
    Remove Directory    ${WORK_ROOT}    recursive=${True}
    Directory Should Not Exist    ${WORK_ROOT}

Read the resource as a state machine. Resolve Workflow Paths calculates paths only. Initialize Or Resume Workflow creates synthetic state exactly once or validates the existing owner marker on a restart. Process One Work Item checks the completion marker before mutation, waits only a bounded five seconds for approval, creates a deterministic outbox path, verifies the output, then writes the durable completion marker and ledger entry.

The interruption hook runs after the checkpoint. That placement is deliberate: the restart exercise proves the second process can observe the marker and skip the already-completed item.

4. Task suite and explicit cleanup task

*** Settings ***
Resource       ../resources/workflow.resource
Suite Setup    Initialize Or Resume Workflow
Task Timeout   30 seconds

*** Tasks ***
Process Synthetic Work Items
    Process Batch Idempotently
*** Settings ***
Resource    ../resources/workflow.resource

*** Tasks ***
Cleanup Completed Workflow
    Verify Owned Existing Workflow State
    Guarded Cleanup Workflow Root

The normal workflow has no teardown that recursively deletes its durable state. That is not an omission: deleting durable state during failure would destroy the information required to resume. Cleanup is a separate operator action that first verifies ownership and completion of every item.

5. First run: process WI-001, checkpoint it, then inject interruption

# Expected result: non-zero because Fatal Error is injected after WI-001 checkpoint.
python -m robot --rpa   --variable RUN_ID:demo-001   --variable INTERRUPT_AFTER:WI-001   --outputdir evidence/first   tasks/workflow.robot

Expected durable state after the failed run: outbox/WI-001.txt exists; state/completed/WI-001.done exists; the durable ledger directory contains exactly one WI-001 entry file; WI-002 remains pending; no WI-002 output exists. Preserve evidence/first/output.xml, log.html, and report.html. The first failure is evidence, not trash.

6. Human approval simulation: create WI-002 decision outside Robot

# Cross-platform Python command executed manually by the operator.
python -c "from pathlib import Path; import tempfile; p=Path(tempfile.gettempdir())/'rf19-rpa-demo-001'/'approvals'/'WI-002.approved'; p.write_text('decision=APPROVE\noperator=training-user\n', encoding='utf-8'); print(p)"

This manual command represents the approval boundary. Robot does not manufacture the decision. The decision record has a work-item identity and synthetic operator identity, and it persists independently of Robot memory.

Optional interactive demonstration: on a local desktop only, Dialogs.Execute Manual Step or Get Selection From User can pause a task. Do not place that path in unattended CI; use a durable approval service/file/queue for operational automation.

7. Resume: completed work is a no-op, pending work advances

python -m robot --rpa   --variable RUN_ID:demo-001   --variable INTERRUPT_AFTER:NONE   --outputdir evidence/resume   tasks/workflow.robot

The log should show WI-001 taking the completion-marker branch and returning without rewriting its checkpoint or ledger row. WI-002 observes its approval, transitions pending → approved → completed, writes outbox/WI-002.txt, then records its checkpoint. The resumed task passes.

8. Third run: prove stable no-op behavior

python -m robot --rpa   --variable RUN_ID:demo-001   --outputdir evidence/idempotent   tasks/workflow.robot

Both items should take the completion-marker branch. The durable ledger directory should still have exactly one deterministic entry file per item. This is a stronger check than “the command passed twice”: it proves repeated invocation does not create additional synthetic business effects.

9. Before/after evidence matrix

Point WI-001 WI-002 Robot evidence
Before first run pending + preapproved pending, no approval none
After injected interruption outbox + completed marker still pending first run FAIL retained
Before resume already complete approval file added manually first failure still preserved
After resume safe no-op outbox + completed marker resume PASS
After third run safe no-op safe no-op idempotency PASS

10. Cleanup is an explicit verified operator action

python -m robot --rpa   --variable RUN_ID:demo-001   --outputdir evidence/cleanup   tasks/cleanup.robot

The cleanup task refuses deletion if a completion marker is missing, if pending/approved markers remain, if the owner file does not match RUN_ID, or if the normalized path does not sit below ${TEMPDIR}. Before deletion it copies the ledger and tree into the Robot output directory.

11. Challenge: choose the correct layer

Add WI-003, but make its approval explicitly REJECT. Decide where rejection should be represented and what final task status should be. A strong design keeps the human decision in durable external state, does not create an outbox/completion marker for a rejected item, and records a diagnosable Robot result rather than silently treating rejection as completion.

Knowledge check

Why does the workflow not delete its temporary root in Suite Teardown?

Why is WI-001 not duplicated on the second run?

What does the approval file own that a Boolean suite variable does not?

Why is Fatal Error injected after the completion marker?

What proves cleanup cannot erase an unrelated temporary directory?

12. Summary and next lesson

You now have a local RPA workflow that survives a failed Robot process and resumes without duplicate effects. Lesson 3 turns those concrete mechanics into design choices: when to use tasks, when to split work, how to choose durable state, and where human review belongs.

Next lesson

RPA Tasks, Long-Running Automation, and Human-in-the-Loop Workflows: Configuration, Design Patterns, and Trade-Offs

Continue with RPA Tasks, Long-Running Automation, and Human-in-the-Loop Workflows: Configuration, Design Patterns, and Trade-Offs. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

References and version anchors

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.