POJO-actor Tutorial Part 2 (First Half): Workflow Language Basics

posted 6 min read

Recap of Part 1

In Part 1, we introduced the basic features of POJO-actor ver 1.x: POJO-actor is a library that allows you to convert ordinary POJOs into actors simply by wrapping them with ActorRef<T>, without modifying existing code. With Virtual Threads available since JDK 21, even commodity hardware can run tens of thousands of actors.

Since most of this is implemented using standard JDK 21+ features, POJO-actor ver 1.x is realized in under 1000 lines of code. POJO-actor ver 1.x was designed to explain the core implementation by implementing only the fundamental concepts.

Version 2.x adds features for practical use.

From Actor to Agent: Introducing the Workflow Engine

In the actor model, each actor has a mailbox (message queue). Each actor retrieves messages from its queue one at a time and processes them sequentially. Meanwhile, multiple actors can operate independently and concurrently. This "sequential internally, parallel externally" structure allows programmers to achieve concurrent processing without worrying about locks.

The actor model is intuitive for humans, so it inherently has various applications. However, traditionally, implementations where one actor occupies one OS thread were common. This constrained the number of actors to the number of CPU cores, making large-scale applications difficult. With Virtual Threads since JDK 21, this constraint is being lifted. Being able to handle tens of thousands of actors opens up applications such as Agent-Based Simulations with many agents, or infrastructure-as-code platforms that monitor and autonomously repair computing resources.

As a foundation for enabling actors to behave autonomously—that is, to act as Agents that observe their environment and act according to their state—a workflow engine has been added in POJO-actor v2.x.

An agent is anything that can be viewed as perceiving its environment through sensors and acting upon that environment through actuators.
— Russell & Norvig, "Artificial Intelligence: A Modern Approach"

actor-WF: Simplifying Workflows

POJO-actor's workflow (actor-WF) is described using a simple model of "send this message to this actor":

  - states: ["start", "processed"]
    actions:
      - actor: dataProcessor    # actor name
        method: process          # method name
        arguments: ["data.csv"]  # arguments

With just three elements—actor, method, and arguments—all steps can be expressed uniformly. This follows the same mental model as tell()/ask() in Java code, and since actors operate independently, parallel execution can be described naturally.

Workflow behavior, conditional branching, and loops are expressed as state transitions. actor-WF is essentially a Turing machine, and complex conditional branching that traditional YAML/JSON-based workflow languages struggle with can be expressed without introducing custom syntax.

How actor-WF Works

actor-WF is essentially a Turing machine. Here we reproduce examples from Turing's original paper using actor-WF.

The following workflow is a Turing machine that computes the binary representation of the rational number 1/3. It writes symbols "0 1 0 1 0 1..." alternately on the tape while cycling through states 1→2→3→4→5→1.

— Charles Petzold, "The Annotated Turing", Wiley Publishing, Inc. (2008)

name: turing83
steps:
- states: ["0", "1"]
  actions:
  - actor: turing
    method: initMachine
- states: ["1", "2"]
  actions:
  - actor: turing
    method: printTape
- states: ["2", "3"]
  actions:
  - actor: turing
    method: put
    arguments: "0"
  - actor: turing
    method: move
    arguments: "R"
- states: ["3", "4"]
  actions:
  - actor: turing
    method: move
    arguments: "R"
- states: ["4", "5"]
  actions:
  - actor: turing
    method: put
    arguments: "1"
  - actor: turing
    method: move
    arguments: "R"
- states: ["5", "1"]
  actions:
  - actor: turing
    method: move
    arguments: "R"

Workflow Behavior

A workflow has multiple Rows (combinations of states and actions) under steps. The Interpreter maintains a current state (initially "0") and operates as follows.

State Matching and Transitions

Each Row's states is in the format [from-state, to-state]. The Interpreter searches from top to bottom for a Row with a from-state matching the current state.

Let's look at the turing83 example:

  1. Initial state: current state = "0"
  2. First Row states: ["0", "1"] matches
  3. Execute actions (initMachine)
  4. On success, update current state to "1"
  5. Search for next matching Row → states: ["1", "2"]
  6. Continue cycling: "1""2""3""4""5""1"→...
Action Execution and Conditional Branching

Each Row's actions are executed from top to bottom. Actions return either success (true) or failure (false).

  • All succeed: Transition to to-state
  • Failure midway: Abort this Row and try the next Row with the same from-state

This mechanism allows conditional branching by listing multiple Rows with the same from-state.

Termination Conditions

The workflow terminates under any of the following:

  • Current state becomes "end" (success)
  • No matching Row is found (failure)
  • Maximum iteration count is reached (failure)

Since turing83 has no "end" state and is an infinite loop, it terminates when the maximum iteration count is reached.

Running the Example

Clone https://github.com/scivicslab/actor-WF-examples, then build and run in the actor-WF-examples directory (requires JDK 21 or later):

git clone https://github.com/scivicslab/POJO-actor
cd POJO-actor
mvn clean install

git clone https://github.com/scivicslab/actor-WF-examples
cd actor-WF-examples
mvn compile
mvn exec:java -Dexec.mainClass="com.scivicslab.turing.TuringWorkflowApp" -Dexec.args="turing83"

Output:

Loading workflow from: /code/turing83.yaml
Workflow loaded successfully
Executing workflow...

TAPE    0    value
TAPE    0    value    0 1
TAPE    0    value    0 1 0 1
TAPE    0    value    0 1 0 1 0 1
TAPE    0    value    0 1 0 1 0 1 0 1
...

Workflow finished: Maximum iterations (50) exceeded

The pattern "0 1 0 1..." is generated on the tape. Since it's an infinite loop, it terminates when maxIterations (50) is reached.

A More Complex Example

The following is a Turing machine that outputs an irrational number. It outputs 001011011101111011111...

— Charles Petzold, "The Annotated Turing", Wiley Publishing, Inc. (2008)

Written in actor-WF:

name: turing87
steps:
- states: ["0", "100"]
  actions:
  - actor: turing
    method: initMachine
- states: ["100", "1"]
  actions:
  - actor: turing
    method: printTape
- states: ["1", "2"]
  actions:
  - actor: turing
    method: put
    arguments: "e"
  - actor: turing
    method: move
    arguments: "R"
  - actor: turing
    method: put
    arguments: "e"
  - actor: turing
    method: move
    arguments: "R"
  - actor: turing
    method: put
    arguments: "0"
  - actor: turing
    method: move
    arguments: "R"
  - actor: turing
    method: move
    arguments: "R"
  - actor: turing
    method: put
    arguments: "0"
  - actor: turing
    method: move
    arguments: "L"
  - actor: turing
    method: move
    arguments: "L"
- states: ["101", "2"]
  actions:
  - actor: turing
    method: printTape
- states: ["2", "2"]
  actions:
  - actor: turing
    method: matchCurrentValue
    arguments: "1"
  - actor: turing
    method: move
    arguments: "R"
  - actor: turing
    method: put
    arguments: "x"
  - actor: turing
    method: move
    arguments: "L"
  - actor: turing
    method: move
    arguments: "L"
  - actor: turing
    method: move
    arguments: "L"
- states: ["2", "3"]
  actions:
  - actor: turing
    method: matchCurrentValue
    arguments: "0"
- states: ["3", "3"]
  actions:
  - actor: turing
    method: isAny
  - actor: turing
    method: move
    arguments: "R"
  - actor: turing
    method: move
    arguments: "R"
- states: ["3", "4"]
  actions:
  - actor: turing
    method: isNone
  - actor: turing
    method: put
    arguments: "1"
  - actor: turing
    method: move
    arguments: "L"
- states: ["4", "3"]
  actions:
  - actor: turing
    method: matchCurrentValue
    arguments: "x"
  - actor: turing
    method: put
    arguments: " "
  - actor: turing
    method: move
    arguments: "R"
- states: ["4", "5"]
  actions:
  - actor: turing
    method: matchCurrentValue
    arguments: "e"
  - actor: turing
    method: move
    arguments: "R"
- states: ["4", "4"]
  actions:
  - actor: turing
    method: isNone
  - actor: turing
    method: move
    arguments: "L"
  - actor: turing
    method: move
    arguments: "L"
- states: ["5", "5"]
  actions:
  - actor: turing
    method: isAny
  - actor: turing
    method: move
    arguments: "R"
  - actor: turing
    method: move
    arguments: "R"
- states: ["5", "101"]
  actions:
  - actor: turing
    method: isNone
  - actor: turing
    method: put
    arguments: "0"
  - actor: turing
    method: move
    arguments: "L"
  - actor: turing
    method: move
    arguments: "L"

This example includes conditional branching using multiple Rows with the same from-state.

# From state 2: if current value is "1", stay in state 2
- states: ["2", "2"]
  actions:
    - actor: turing
      method: matchCurrentValue
      arguments: "1"
    # ... subsequent actions

# From state 2: if current value is "0", go to state 3
- states: ["2", "3"]
  actions:
    - actor: turing
      method: matchCurrentValue
      arguments: "0"
  1. If matchCurrentValue("1") returns true → Execute first Row, remain in state 2
  2. If matchCurrentValue("1") returns false → Abort this Row, try next Row
  3. If matchCurrentValue("0") returns true → Transition to state 3
Running the Example
git clone https://github.com/scivicslab/POJO-actor
cd POJO-actor
mvn clean install

git clone https://github.com/scivicslab/actor-WF-examples
cd actor-WF-examples
mvn compile
mvn exec:java -Dexec.mainClass="com.scivicslab.turing.TuringWorkflowApp" -Dexec.args="turing87"

Output:

Loading workflow from: /code/turing87.yaml
Workflow loaded successfully
Executing workflow...

TAPE    0    value
TAPE    0    value    ee0 0 1 0
TAPE    0    value    ee0 0 1 0 1 1 0
TAPE    0    value    ee0 0 1 0 1 1 0 1 1 1 0
TAPE    0    value    ee0 0 1 0 1 1 0 1 1 1 0 1 1 1 1 0

Workflow finished: Maximum iterations (200) exceeded

Summary

What we covered in this tutorial:

  1. Basic workflow structure: Combinations of states and actions
  2. State transitions: Control flow through transitions from from-state to to-state
  3. Conditional branching: Achieved by listing multiple Rows with the same from-state
  4. Loops: Achieved through state cycles (e.g., 1→2→3→4→5→1)

actor-WF is based on the same design philosophy as Turing machines. Any complexity of processing can be expressed through combinations of state transitions and actions.

Next Steps

To create your own workflows, you need to implement an adapter (IIActorRef) to call POJOs from workflows. See the following for details:

References

2 Comments

1 vote
0
0 votes

More Posts

POJO-actor Tutorial Part 2 (Second Half): Creating Workflows

devteam2512 - Dec 31, 2025

POJO-actor Tutorial Part 2-3: Improving the Workflow API — Introducing the @Action Annotation

devteam2512 - Jan 28

actor-IaC: POJO-actor Workflow Based Infrastructure as Code for Cluster Management (Part 1)

devteam2512 - Jan 28

Local-First: The Browser as the Vault

Pocket Portfolioverified - Apr 20

POJO-actor v1.0: A Lightweight Actor Model Library for Java

devteam2512 - Dec 22, 2025
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

2 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!