# 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`
```bash
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()`.
```rust
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`](<https://github.com/yaojianpin/acts/tree/main/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](https://github.com/rhaiscript/rhai)
```yml
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.
```yml
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
```yml
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`
```yml
name: model name
jobs:
- id: job1
- id: job2
needs: [ "job1" ]
```
### Steps
Use `steps` to add step to the job
```yml
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
```yml
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
```yml
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.
```yml
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`
```yml
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`).
```yml
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.
```yml
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
```