The Workflow Language (WL) defines a data format that expresses actionable steps and control flow instructions to act as a data-driven scripting language for solving simple tasks in our platform.
A workflow is an object that has an array of steps
and possibly other information about how to handle execution (TBD).
type Step = Action | Control;
interface Workflow {
steps: Step | Step[];
// TBD: options...
timeout?: number;
}
Actions are used to modify external state as part of a workflow. The available actions and their respective
configuration are not necessarily part of the WL specification. The only required field is type
s.t. the evaluation
engine can dispatch accordingly.
interface Action {
type: string;
result?: ResultDescription;
}
If the result of the action should be available to later steps, result
describes how to transform the data and under
what name to store it.
Within the transform
logic, the action's immediate result (e.g. a response body) is available under action.result
.
interface ResultDescription {
as: string;
transform?: JsonLogic;
}
Executes an action using the BXDK/Internal API SDK. Operations are encoded using entity
, operation
and a list of
args
(arguments to the function described by entity and operation). The concrete values must be looked up using the BXDK documentation.
interface ApiAction extends Action {
type: "api";
entity: string;
operation: string;
// should result in type: (string | number | boolean | Record<string, string | number | boolean>)[];
args: JsonLogic;
}
Currently, no special attention is being paid to authentication, neither:
- That the correct client_id is used in the correct place in the action's args
- That the user who created the batch operation has the required permissions to execute the modification
Executes a generic HTTP action.
interface HTTPAction extends Action {
type: "http";
url: string;
method?: string;
path?: string | JsonLogic[];
query?: JsonLogic;
headers?: Record<string, string>;
body?: string | JsonLogic;
auth_token?: string | JsonLogic;
}
Mandatory request target, including schema (http/https), hostname, path and query.
If the WORKFLOW_ALLOWED_HTTP_HOSTS
environment variable is set to a comma-separated list of host
names in the minimatch syntax, only these hosts are allowed
to be contacted.
HTTP request method to use. Supports get
(the default), post
, put
, patch
, and delete
.
An optional, additional argument for path which can be a list of JsonLogic segments that will be
joined by /
.
Example:
"path": ["api", "v1", %{"var": "params.entityType"}]
If url
already contains a path, the value of this argument is appended to url
's path:
- If
path
is absolute (starts with/
), it fully replaces theurl
's path. - If
path
is relative (starts with a segment or./
), it is appended. Ifurl
doesn't end in/
,path
replaces the last segment (or multiple ifpath
starts with../..
)
JsonLogic that resolves to an object that will be encoded to query string, values can be strings, numbers, booleans, or arrays of these types.
Binary values will be passed through as-is.
If url
already contains a query string, query
will be appended to it.
Customize headers by specifying a map of key-value-pairs. Does not support JsonLogic. All header names will be downcased before sending to allow overriding the default headers.
By default, accept: application/json
will be sent (and a content-type
header depending on body
).
Request body. Supported for all methods, but only recommended for POST, PUT and PATCH.
Can be a (pre-encoded) binary or a JsonLogic that resolves to either a map or a list. In the later
case, content-type: application/json
will be added to the request headers.
String or JsonLogic. If set to/resolves to a string, it will be used as token in an Authorization: Bearer <TOKEN>
header.
Example:
"auth_token": {"var": "params.user_token"}
Since linear execution of a series of actions is not sufficient to complete even moderately complex tasks, control elements can be used to dynamically customize the workflow.
type Control = If | Loop | Yield;
The conditional executes the then
steps/workflow if the if
condition is true, or the optional else
steps/workflow
otherwise. If a workflow is given for then
or else
branch, its options might be ignored by the engine (TBD).
interface If {
if: JsonLogic;
then: Workflow | Step | Step[];
else?: Workflow | Step | Step[];
}
The loop control executes the sub-workflow (do
) for every item of the list extracted using the loop
expression.
interface Loop {
loop: JsonLogic;
element?: string;
do: Workflow | Step | Step[];
}
To sub-workflows, the current loop element is available as variable loop.element
and, if given, under the name defined by element
. Additionally, the current element's index is available as loop.element_index
and optionally <element>_index
.
interface LoopContext {
"loop.element": any;
"loop.element_index": number;
"<element>"?: any;
"<element>_index"?: any;
}
When nesting loops, the inner loop's element will be available under loop.element
. To keep access to the outer loop's current element, its element
option needs to be used to create a named binding.
The yield control emits data to the caller of the workflow. The meaning of yielded data depends entirely on the caller and might even change throughout the workflow.
For example, a workflow could yield items that need to be worked on later (and the caller would collect them in a list) or it could yield metadata about the current workflow which gets collected in an object.
interface Yield {
yield: JsonLogic;
}
Context values and variables can be used by steps and controls through the use of the JsonLogic { var: "path" }
operator.
The workflow's caller might give a predefined set of params to the workflow. These are available under the params
key
(e.g. params.client_id
).
Any (intermediately) stored values, e.g. from action results or loop bindings are directly stored under the given name
(e.g. updateTag.updated_at
or currentTag
).