Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/factor18/flo

Flow based programming for elixir
https://github.com/factor18/flo

elixir fbp flow-based-programming workflow-engine

Last synced: 28 days ago
JSON representation

Flow based programming for elixir

Awesome Lists containing this project

README

        

# Flo
Extensible workflow orchestration framework

### Installation

The package can be installed by adding `flo` to your list of dependencies in mix.exs:

```elixir
def deps do
[{:flo, "~> 0.2"}]
end
```

Add to list of applications

```elixir
extra_applications: [:logger, :flo]
```

### Flo Framework
Flo exposes two [behaviours](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#behaviours) which can be used to create your own triggers and components
- `Component` is a behaviour for exposing common application logic in a reusable manner. Think of this as a function, such as write to database, publish to Kafka, etc that can be used by all Flo apps
- `Trigger` is a behaviour for building event-consumers that trigger workflows. The Kafka subscriber is an example of a trigger



#### Workflow is a combination of Trigger(s) and Components

- Triggers
- Invokes the workflow
- Can be invoked on its own (interval, cron etc) or by external events (http, queues etc)
- Components:
- Definition of a task
- Have a common interface which allows them to be interconnected
- Connections:
- Defines the order of invokation
- Can be conditional
- Allows branching and merging

#### Component
Here is the implementation of a component which returns a dog pic of a given breed

`@name` and `@scope` are used for referencing this component in a workflow

`@inports` are a list of properties which can be consumed by the component

`@outports` defines the responses from this component, which can be consumed by other components

```elixir
defmodule Flo.Core.Component.Dog do
alias Flo.{Port, Context, Outports}

@name "dog"

@scope "core"

@inports [
%Port{required: true, name: "breed", schema: %{"type" => "string"}}
]

@outports %Outports{
default: [
%Port{name: "url", required: true, schema: %{"type" => "string"}}
],
additional: %{
"error" => [
%Port{name: "message", required: true, schema: %{"type" => "string"}}
]
}
}

use Flo.Component

@impl true
def run(%Context.Element{inports: inports}) do
breed = inports |> Map.get("breed")

url = "https://dog.ceo/api/breed/#{breed}/images/random/1"

case HTTPoison.get(url) do
{:ok, %{status_code: 200, body: body}} ->
url = Jason.decode!(body) |> Map.get("message") |> Enum.at(0)
%Context.Outports{outcome: "default", value: %{"url" => url}}
_ ->
%Context.Outports{outcome: "error", value: %{"message" => "No dog :-("}}
end
end
end

```
Now this component can be used in all of your workflows

#### Trigger
Here is the implementation of a trigger
`@name` and `@scope` are used for referencing this component in a workflow

`@configs` are a list of configs which can be consumed by the component

`@outports` defines the responses from this component, which can be consumed by other components

`initialize` function will be invoked with a callback function `start`

The `start` function will receive the outports map and will start the workflow whenever called

`Flo.Trigger` uses a `GenServer` behind the scenes, the return of `initialize` will be the return of GenServer's `init` callback

```elixir
defmodule Flo.Core.Trigger.AMQP do
alias Flo.{Port, Context, Outports}

@name "amqp"

@scope "core"

@configs [
%Port{
name: "queue",
required: true,
schema: %{"type" => "string"}
},
%Port{
required: true,
name: "connection_string",
schema: %{"type" => "string"}
}
]

@outports %Outports{
default: [
%Port{
required: true,
name: "payload",
schema: %{"type" => "string"}
}
]
}

use Flo.Trigger

@impl true
def initialize(%Context.Stimulus{configs: configs}, start) do
queue = configs |> Map.get("queue")
connection_string = configs |> Map.get("connection_string")

{:ok, conn} = AMQP.Connection.open(connection_string)
{:ok, chan} = Channel.open(conn)

AMQP.Queue.subscribe(chan, queue, fn payload, _meta ->
start.(%{"payload" => payload})
end)

{:ok, %{start: start}}
end
end

```

#### Workflow
![Image of Workflow](https://user-images.githubusercontent.com/11179580/123558812-dfe03f80-d7b5-11eb-8117-800168b87d15.png)

Here is a sample which implements the above workflow

`stimuli` are the list of triggers

`elements` are the list of components

`connections` form the flow between components

```elixir
%Flo.Workflow{
name: "sample",
description: "Sample Flow",
stimuli: [
%Flo.Stimulus{
ref: "interval-trigger",
inports: %{},
scope: "core",
name: "interval",
configs: %{
"delay" => %Flo.Script{language: Flo.Script.Language.vanilla(), source: 5000}
}
}
],
elements: [
%Flo.Element{
ref: "dog-api",
name: "dog",
scope: "core",
inports: %{
"breed" => %Flo.Script{
source: "shihtzu",
language: Flo.Script.Language.vanilla(),
}
}
},
%Flo.Element{
ref: "delay",
name: "delay",
scope: "core",
inports: %{
"delay" => %Flo.Script{
source: 1500,
language: Flo.Script.Language.vanilla(),
}
}
},
%Flo.Element{
ref: "url-log",
name: "log",
scope: "core",
inports: %{
"delay" => %Flo.Script{
source: "Check the photo {{elements.dog-api.outports.url.value}}",
language: Flo.Script.Language.liquid(),
}
}
},
%Flo.Element{
ref: "error-log",
name: "log",
scope: "core",
inports: %{
"delay" => %Flo.Script{
source: "Error occured: {{elements.dog-api.outports.error.message.value}}",
language: Flo.Script.Language.liquid(),
}
}
},
%Flo.Element{
ref: "end-log",
name: "log",
scope: "core",
inports: %{
"delay" => %Flo.Script{
source: "Done!!",
language: Flo.Script.Language.vanilla(),
}
}
}
],
connections: [
%Flo.Connection{
source: "dog-api",
outcome: "default",
destination: "delay",
},
%Flo.Connection{
source: "delay",
outcome: "default",
destination: "url-log",
},
%Flo.Connection{
source: "dog-api",
outcome: "error",
destination: "error-log",
},
%Flo.Connection{
source: "url-log",
outcome: "default",
destination: "end-log",
},
%Flo.Connection{
source: "error-log",
outcome: "default",
destination: "end-log",
}
]
}
|> Flo.WorkflowRegistry.register()
```

A component will be executed when all of the incoming connections are resolved

A connection can be in three states `INITIAL`, `RESOLVED`, `DISABLED`

If there are multiple connections to the component, the component will be executed only when all of the connections are in `RESOLVED` and `DISABLED` state and at least one connection should be in resolved state

If a connection is `DISABLED`, it will recursively disable all connections till a component is found which has a connection still in `INITIAL` or `RESOLVED` state

### TODO
- [x] Workflow execution
- [x] Branching and merging
- [x] Conditional branching
- [x] Multiple outcomes for components
- [ ] Loops
- [ ] Visual Editor
- [ ] Sub flows
- [ ] Error handling
- [ ] Documentation

### Contributing
Request a new feature by creating an issue or create a pull request with new features or fixes.

### License
`Flo` source code is released under Mozilla Public License 2.0.
Check [LICENSE](https://github.com/factor18/flo/blob/main/LICENSE) file for more information