acts 0.6.0

a fast, tiny, extensiable workflow engine
Documentation

Acts workflow engine

acts is a fast, tiny, extensiable workflow engine, which provides the abilities to execute workflow based on yml model.

The yml workflow model is not as same as the tranditional workflow flow. such as bpmn. The yml format is inspired by Github actions. The main point of this workflow is to create a top abstraction to run the workflow logic and interact with the client via act node.

Every user's action can be regarded as a abstract act. these acts can be generated by some rules. such as for or catches.

This workflow engine focus on the workflow logics itself and message distributions. the complex business logic will be completed by act via the act message.

Key Features

Fast

Uses rust to create the lib, there is no virtual machine, no db dependencies. The feature local_store uses the rocksdb to make sure the store performance.

Running benches\workflow.rs start_workflow time: [842.58 µs 876.99 µs 912.59 µs]

Tiny

The lib size is only 3.5mb (no local_store), you can use Adapter to create external store.

Extensiable

Supports for extending the plugin Supports for creating external store

Installation

The easiest way to get the latest version of acts is to install it via cargo

cargo add acts

Quickstart

  1. Start the workflow engine by engine.start.
  2. Load a yaml model to create a workflow.
  3. Deploy the model in step 2 by engine.manager().
  4. Config events by engine.emitter().
  5. Start the workflow by engine.executor().
use acts::{Engine, Vars, Workflow};

#[tokio::main]
async fn main() {
    let engine = Engine::new();
    engine.start();

    let text = include_str!("../examples/simple/model.yml");
    let mut workflow = Workflow::from_yml(text).unwrap();

    let executor = engine.executor();
    engine.manager().deploy(&workflow).expect("fail to deploy workflow");

    let mut vars = Vars::new();
    vars.insert("input".into(), 3.into());
    vars.insert("pid".to_string(), "w1".into());
    executor.start(&workflow.id, &vars);
    let emitter = engine.emitter();

    emitter.on_start(|e| {
        println!("start: {}", e.start_time);
    });

    emitter.on_message(|e| {
        println!("message: {:?}", e);
    });

    emitter.on_complete(|e| {
        println!("outputs: {:?} end_time: {}", e.outputs(), e.end_time);
    });

    emitter.on_error(|e| {
        println!("error on proc id: {} model id: {}", e.pid, e.mid);
    });
}

Examples

Please see examples

Model Usage

The model uses the yaml file to create, there are different type of node, which is constructed by [Workflow], [Job], [Branch], [Step] and [Act]. Every workflow can have more jobs, every job can have more steps, a step can have more branches and a branch can have if property to judge the condition.

The env property can be set the initialzed vars in workflow, in the step's run scripts, you can use env moudle to get(env.get) or set(env.set) the value

The run property is the script based on rhai script

name: model name
jobs:
  - id: job1
    env:
      value: 0
    steps:
      - name: step 1
        run: |

          print("step 1")

      - name: step 2
        branches:
          - name: branch 1
            if: ${ env.get("value") > 100 }
            run: |

                print("branch 1");

          - name: branch 2
            if: ${ env.get("value") <= 100 }
            steps:
                - name: step 3
                  run: |

                    print("branch 2")
            

Outputs

In the [Workflow], you can set the outputs to output the env to use.

name: model name
outputs:
  output_key:
jobs:
  - id: job1
    steps:
      - name: step1
        run: |

          env.set("output_key", "output value");

Actions

Add workflow actions to create custom event with client

name: model name
actions:
  - name: fn1
    id: fn1
    on: 
      - state: created
        nkind: workflow
      - state: completed
        nkind: workflow
  - name: fn2
    id: fn2
    on: 
      - state: completed
        nid: step2

  - name: fn3
    id: fn3
    on: 
      - state: completed
        nid: step3
    inputs:
      a: ${ env.get("value") }
jobs:
  - id: job1
    steps:
      - name: step1
      - name: step2
      - name: step3

Jobs

Use jobs to add multiple job to a workflow, the job and run concurrently. Or set it one by one running orderly by use the property needs

name: model name
jobs:
  - id: job1
  - id: job2
    needs: [ "job1" ]

Steps

Use steps to add step to the job

name: model name
jobs:
  - id: job1
    steps:
      - id: step1
        name: step 1
      - id: step2
        name: step 2

Branches

Use branches to add branch to the step

name: model name
jobs:
  - id: job1
    steps:
      - id: step1
        name: step 1
        branches:
          - id: b1
            if: env.get("v") > 0
            steps: 
              - name: step a
              - name: step b
          - id: b2
            else: true
            steps:
              - name: step c
              - name: step d
      - id: step2
        name: step 2

Acts

Use acts to create act to interact with client

name: model name
outputs:
  output_key:
jobs:
  - id: job1
    steps:
      - name: step1
        acts:
          - id: init
            name: my act init
            inputs:
              a: 6
            outputs:
              c:

1. for

There is a example to use for to generate acts, which can wait util calling the action to complete.

name: model name
jobs:
  - id: job1
    steps:
      - name: step1
        acts:
          - for:
              by: any
              in: |

                let a = ["u1"];
                let b = ["u2"];
                a.union(b)

It will generate the user act and send message automationly according to the in collection. The by tells the workflow how to pass the act there are several by rules.

  • by
  1. all to match all of the acts to complete

  2. any to match any of the acts to complete

  3. some(rule) to match some acts by giving rule name. If there is some rule, it can also generate a some act to ask the client to pass or not.

  4. ord or ord(rule) to generate the act one by one. If there is order rule, it can also generate a rule act to sort the collection.

  • in A collection to generate the acts.

The code act.role("test_role") uses the role rule to get the users through the role test_role

in: |

    let users = act.role("test_role");
    users

The following code uses the relate rule to find the user's owner of the department (d.owner).

users: |

    let users = act.relate("user(test_user).d.owner");
    users

2. catches

Use the catches to capture the act error and start a new act to run.

name: a example to catch act error
id: catches
jobs:
  - name: job1
    id: job1
    steps:
      - name: prepare
        id: prepare
        acts:
          - id: init
      - name: step1
        id: step1
        acts:
          - id: act1
            catches:
              - id: catch1
                err: err1
              - id: catch2
                err: err2
              - id: catch_others
      - name: final
        id: final