Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - box-id/workflow_engine: Workflow Engine Implementation in Elixir · GitHub
Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - box-id/workflow_engine: Workflow Engine Implementation in Elixir · GitHub
Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - box-id/workflow_engine: Workflow Engine Implementation in Elixir · GitHub
Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - box-id/workflow_engine: Workflow Engine Implementation in Elixir · GitHub
Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - box-id/workflow_engine: Workflow Engine Implementation in Elixir · GitHub
Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - box-id/workflow_engine: Workflow Engine Implementation in Elixir · GitHub
Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - box-id/workflow_engine: Workflow Engine Implementation in Elixir · GitHub
Skip to content

Repository files navigation

WorkflowEngine

Workflow Engine evaluates a series of actions and control instructions to perform requests against our internal API (using the BXDK package), tag data API, external HTTP APIs etc. using a JSON based Workflow Language.

result_state=WorkflowEngine.evaluate(workflow,params: %{context: %{}},auth: MyApp.WorkflowEngine,json_logic: MyService.JsonLogic,actions: %{"foo"=>MyApp.FooAction})

Installation

This package can be installed by adding workflow_engine to your list of dependencies in mix.exs:

defdepsdo[{:workflow_engine,github: "box-id/workflow_engine",tag: "0.1.0"}]end

Extending Workflow Engine

To extend Workflow Engine with custom actions, implement the WorkflowEngine.Action behaviour.

This is a minimal example of a custom action that multiplies a value by a given factor and stores the result in the workflow state:

defmoduleMyApp.FooActiondo@behaviourWorkflowEngine.Action@impltruedefexecute(workflow_state,%{"type"=>"multiply"}=step)do# Implement your action logic heremultiply_by=get_required(step,"multiply_by")source_key=get_required(step,"source_key")value=Map.get(workflow_state,source_key,1)new_state=Map.put(workflow_state,"multiply_result",value*multiply_by){:ok,{new_state,nil}}rescue# Wrap all error messages & add current stateeinWorkflowEngine.Error->reraiseWorkflowEngine.Error,[message: "FooAction: "<>e.message,state: state],__STACKTRACE__enddefpget_required(step,key)docaseMap.fetch(step,key)do{:ok,value}whennotis_nil(value)->value_->raiseWorkflowEngine.Error,message: "Missing required step parameter \"#{key}\"."endendend

Then, setup the action in a customized module that implements the WorkflowEngine:

defmoduleMyAppNamespace.WorkflowEnginedodefevaluate(workflow,opts\\[])dostate=%WorkflowEngine.State{vars: Keyword.fetch!(opts,:vars),actions: %{"multiply"=>MyApp.FooAction,}}WorkflowEngine.evaluate(state,workflow)endend

WorkflowEngine.State Attributes

  • vars: A map of variables that can be used in the workflow.
  • json_logic_mod: The module implementing the JSON Logic evaluation logic.
  • auth: A module implementing the WorkflowEngine.Auth behaviour (or nil). See Authentication below.
  • actions: A map of action types to their respective modules. This allows you to define custom actions that can be used in workflows.

Authentication

Authentication for workflow actions is handled via an optional callback. Instead of hardcoding tokens or credentials, you implement an authenticate/2 function that is called on demand when an action needs auth.

Setup

use WorkflowEngine.Auth in your wrapper module and override authenticate/2:

defmoduleMyApp.WorkflowEnginedouseWorkflowEngine.Auth@implWorkflowEngine.Authdefauthenticate("tables",_target)do{:ok,{:bearer,MyApp.M2MAuth.create_service_token()}}enddefauthenticate(_type,_target),do: {:ok,nil}end

Callback signature

@callbackauthenticate(type::binary(),target::any())::{:ok,any()}|{:error,any()}
  • type: the step's "type" string (e.g. "http")
  • target: action-specific target info (for the built-in HTTP action this is the full URL string)

Return values

ReturnEffect
{:ok, nil}No authentication is applied
{:ok, auth}Auth value passed as the :auth option to Req.new/1
{:error, reason}Raises a WorkflowEngine.Error

The auth value supports all formats accepted by Req's :auth option, e.g. {:bearer, token}, {:basic, string}, etc. See the Req :auth docs for the full list.

Using auth in custom actions

Custom actions can call WorkflowEngine.Auth.get_auth/3 to obtain credentials:

defexecute(state,%{"type"=>"my_action"}=step)dotarget=get_target(step)caseWorkflowEngine.Auth.get_auth(state,"my_action",target)do{:ok,nil}-># proceed without auth{:ok,credentials}-># use credentials{:error,reason}-># handle errorendend

Precedence

For the built-in HTTP action, a step-level auth_token always takes precedence over the callback. This allows individual workflow steps to override the default auth when needed.

Error Handling

Since workflows are dynamic (and potentially user-provided), Workflow Engine and its actions need to take great care of handling errors in a transparent way.

Workflow Engine uses the {:error, %WorkflowEngine.Error{}} return type if something unexpected ocurred. Workflow Engine should never return an error tuple with another data type at the second position.

Also, no exceptions should be raised during workflow evaluation. However, due to the manifold ways invalid configuration or data could be supplied, it can't be fully ruled out. These cases are considered a bug, though, and are worth fixing by expanding input validation and error handling.

message.

Errors contain a message binary, which is a human-readable description of the error, including the formatted instruction pointer (to hint to the position in the workflow that likely caused the error) and the configuration of the workflow step that failed.

Depending on the actions and configuration of a workflow, message could contain sensitive information and might thus not be suitable for displaying to users.

state

The state field allows introspecting the state of the Workflow Engine at the time the error occurred.

recoverable

The boolean recoverable field gives a best-effort estimation about whether retrying the workflow could lead to a positive/error-free outcome or not. The decision is made based on the reason for the error.

For example, retrying workflows with invalid configuration such as an invalid step descriptions, unknown action names, malformed JsonLogic etc., will never lead to a successful result. In such cases, errors are tagged with recoverable: false.

Softer errors, that typically originate from an external system such as a failed HTTP request or a syntax error in a CSV file, are marked with recoverable: true. For these errors, the caller can decide whether it wants to automatically or manually retry running the workflow.

Tests

Run tests using mix test or, during development, mix test.watch.

Tests touching external systems should be tagged with @tag :external_service s.t. they are (by default) skipped, which keeps the execution of the test suite fast and reliable.

To include those tests as well, use mix test.including_external or the --include external_service flag.

About

Workflow Engine Implementation in Elixir

Resources

Stars

4 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages