Compositions

Composer makes it possible to assemble actions into rich workflows called compositions. An example composition is described in ../README.md.

Control flow

Compositions can express the control flow of typical imperative programming language: sequences, conditionals, loops, structured error handling. This control flow is specified using combinator methods such as:

  • composer.sequence(firstAction, secondAction)
  • composer.if(conditionAction, consequentAction, alternateAction)
  • composer.try(bodyAction, handlerAction)

Parallel constructs are also available.

Combinators are described in COMBINATORS.md.

Composition objects

Combinators return composition objects, i.e., instances of the Composition class.

Parameter objects and error objects

A composition, like any action, accepts a JSON dictionary (the input parameter object) and produces a JSON dictionary (the output parameter object). An output parameter object with an error field is an error object. A composition fails if it produces an error object.

By convention, an error object returned by a composition is stripped from all fields except from the error field. This behavior is consistent with the OpenWhisk action semantics, e.g., the action with code function main() { return { error: 'KO', message: 'OK' } } outputs { error: 'KO' }.

Error objects play a specific role as they interrupt the normal flow of execution, akin to exceptions in traditional programming languages. For instance, if a component of a sequence returns an error object, the remainder of the sequence is not executed. Moreover, if the sequence is enclosed in an error handling composition like a composer.try(sequence, handler) combinator, the execution continues with the error handler.

Reserved parameter name

The field name $composer is reserved for composer internal use. Compositions and composed actions should not expect or return parameter objects with a top-level field named $composer.

Data flow

The invocation of a composition triggers a series of computations (possibly empty, e.g., for the empty sequence) obtained by chaining the components of the composition along the path of execution. The input parameter object for the composition is the input parameter object of the first component in the chain. The output parameter object of a component in the chain is typically the input parameter object for the next component if any or the output parameter object for the composition if this is the final component in the chain.

For example, the composition composer.sequence('triple', 'increment') invokes the increment action on the output of the triple action.

Some combinators however are designed to alter the default flow of data. For instance, the composer.merge('myAction') composition merges the input and output parameter objects of myAction.

Components

Components of a compositions can be actions, JavaScript functions, or compositions.

JavaScript functions can be viewed as simple, anonymous actions that do not need to be deployed and managed separately from the composition they belong to. Functions are typically used to alter a parameter object between two actions that expect different schemas, as in:

composer.sequence('getUserNameAndPassword', params => ({ key = btoa(params.user + ':' + params.password) }), 'authenticate')

Combinators can be nested, e.g.,

composer.if('isEven', 'half', composer.sequence('triple', 'increment'))

Compositions can reference other compositions by name. For instance, assuming we deploy the sequential composition of the triple and increment actions as the composition tripleAndIncrement, the following code behaves identically to the previous example:

composer.if('isEven', 'half', 'tripleAndIncrement')

The behavior of this last composition would be altered if we redefine the tripleAndIncrement composition to do something else, whereas the first example would not be affected.

Embedded action definitions

A composition can embed the definitions of none, some, or all the composed actions as illustrated in demo.js:

composer.if(
    composer.action('authenticate', { action: function ({ password }) { return { value: password === 'abc123' } } }),
    composer.action('success', { action: function () { return { message: 'success' } } }),
    composer.action('failure', { action: function () { return { message: 'failure' } } }))
)

Deploying such a composition deploys the embedded actions.

Conductor actions

Compositions are implemented by means of OpenWhisk conductor actions. Compositions have all the attributes and capabilities of an action, e.g., default parameters, limits, blocking invocation, web export. Execution traces and limits of compositions follow from conductor actions.

The conductor action code for a composition may be obtained by means of the generate method of the conductor module or using the compose command with the --js flag. The conductor action code may be deployed using, e.g., the OpenWhisk CLI.

compose demo.js --js > demo-conductor.js
wsk action create demo demo-conductor.js -a conductor true

The conductor annotation must be set on conductor actions.