JavaScript Composer API

The composer module makes it possible to compose actions programmatically. The module is typically used as illustrated in samples/demo.js:

const composer = require('@openwhisk/composer')

// author action composition
const app = composer.if('authenticate', /* then */ 'welcome', /* else */ 'login')

// compile action composition
composer.compile(app, 'demo.json')

When using the programming shell fsh however, there is no need to instantiate the module or invoke compile explicitly. The demo.js file can be shortened to:

composer.if('authenticate', /* then */ 'welcome', /* else */ 'login')

and used as follows:

$ fsh app create demo demo.js

This example program composes actions authenticate, welcome, and login using the composer.if composition method. The composer.compile method produces a JSON object encoding the composition and optionally writes the object to a file. The compiled composition is shown in samples/demo.json.

To deploy and run this composition and others, please refer to README.md.

Compositions and Compiled Compositions

All composition methods return a composition object. Composition objects must be compiled using the composer.compile method before export or invocation. Compiled compositions objects are JSON objects that obey the specification described in FORMAT.md. They can be converted to and from strings using the JSON.stringify and JSON.parse methods. In contrast, the format of composition objects is not specified and may change in the future.

composer.compile(composition[, filename])

composer.compile(composition, filename) compiles the composition object to its JSON representation and writes the JSON object to the file filename if specified. It returns the compiled composition object.

Tasks

Composition methods compose tasks. A task is one of the following:

TypeDescriptionExamples
function taska JavaScript function expressionparams => params or function (params) { return params }
action taskan OpenWhisk action'/whisk.system/utils/echo'
compositiona compositioncomposer.retry(3, 'connect')
compiled compositiona compiled compositioncomposer.compile(composer.retry(3, 'connect'))

Function expressions occurring in action compositions cannot capture any part of their declaration environment. They may however access and mutate variables in an environment consisting of the variables declared by the composer.let composition method as discussed below.

Actions are specified by name. Fully qualified names and aliases are supported following the usual conventions.

A function task applies the function to the parameter object. An action task the invokes the action with the given name on the parameter object. A composition task or compiled composition task applies the composition to the parameter object.

Parameter Objects and Error Objects

A task is a function that consumes a JSON dictionary (the input parameter object) and produces a JSON dictionary (the output parameter object). An output parameter object of a task with an error field is an error object. A task fails if it produces an error object.

Values returned by constant and function tasks are converted to JSON using JSON.stringify followed by JSON.parse. Values other than JSON dictionaries are replaced with a dictionary with a unique value field with the converted value. For instance, the task 42 outputs the JSON dictionary { value: 42 }.

By convention, an error object returned by a task is stripped from all fields except from the error field. For instance, the task () => ({ error: 'KO', message: 'OK' }) outputs the JSON dictionary { error: 'KO' }. This is to be consistent with the OpenWhisk action semantics, e.g., the action with code function main() { return { error: 'KO', message: 'OK' } } outputs { error: 'KO' }.

Composition Methods

The following composition methods are currently supported:

CompositionDescriptionExample
tasksingle taskcomposer.task('sayHi', { input: 'userInfo' })
valueconstant valuecomposer.value({ message: 'Hello World!' })
sequencesequencecomposer.sequence('getLocation', 'getWeatherForLocation')
letvariablescomposer.let('n', 42, ...)
ifconditionalcomposer.if('authenticate', /* then */ 'welcome', /* else */ 'login')
whileloopcomposer.while('needMoreData', 'fetchMoreData')
tryerror handlingtry('DivideByN', /* catch */ 'NaN')
repeatrepetitionrepeat(42, 'sayHi')
retryerror recoveryretry(3, 'connect')
retainparameter retentioncomposer.retain('validateInput')

composer.task(task[, options])

composer.task(task, options) is a composition with a single task task. The optional options parameter may alter the task behavior as follows:

  • If options.merge evaluates to true the output parameter object for the composition is obtained by merging the output parameter object for the task into the input parameter object (unless the task produces an error object).

    For instance, the composition composer.task(42, { merge: true }) invoked on the input parameter object { value: 0, message: 'OK' } outputs { value: 42, message: 'OK' }.

  • Alternatively if options.output is defined the output parameter object for the composition is obtained by assigning the output parameter object for the task to the options.output field of the input parameter object for the composition. Additionally, if options.input is defined the input parameter for the task is only the value of the field options.input of the input parameter object for the composition as opposed to the full input parameter object.

    For instance, the composition composer.task(({n}) => ({ n: n+1 }), { input: 'in', output: 'out' }) invoked on the input parameter object { in: { n: 42 } } outputs { in: { n: 42 }, out: { n: 43 } }.

    If the value of the options.input field is not a JSON dictionary is it replaced with a dictionary with a unique field value with the field's value.

composer.value(json)

composer.value(json) outputs json if it is a JSON dictionary. If json is not a JSON dictionary is it replaced with a dictionary with a unique field value with value json.

The json value may be computed at composition time. For instance, the following composition captures the composition time:

composer.value(Date())

composer.sequence(task_1, task_2, ...)

composer.sequence(task_1, task_2, ...) runs a sequence of tasks (possibly empty).

The input parameter object for the composition is the input parameter object of the first task in the sequence. The output parameter object of one task in the sequence is the input parameter object for the next task in the sequence. The output parameter object of the last task in the sequence is the output parameter object for the composition.

If one of the tasks fails, the remainder of the sequence is not executed. The output parameter object for the composition is the error object produced by the failed task.

An empty sequence behaves as a sequence with a single function task params => params. The output parameter object for the empty sequence is its input parameter object unless it is an error object, in which case, as usual, the error object only contains the error field of the input parameter object.

composer.let(name, value, task_1, task_2, ...)

composer.let(name, value, task_1_, _task_2_, ...) declares a new variable with name name and initial value value and runs a sequence of tasks in the scope of this definition.

Variables declared with composer.let may be accessed and mutated by functions running as part of the following sequence (irrespective of their place of definition). In other words, name resolution is dynamic. If a variable declaration is nested inside a declaration of a variable with the same name, the innermost declaration masks the earlier declarations.

For example, the following composition invokes task task repeatedly n times.

composer.let('i', n, composer.while(() => i-- > 0, task))

Observe the first argument to the let composition is the quoted variable name 'i', whereas occurrences of the variable in the let body are not quoted. We recommend expanding let compositions as follows to avoid confusing code editors:

let i = 'i'; composer.let(i, n, composer.while(() => i-- > 0, task))

Variables declared with composer.let are not visible to invoked actions. However, they may be passed as parameters to actions as for instance:

let n = 'n'; composer.let(n, 42, () => ({n}), '/whisk.system/utils/echo', (params) => { n = params.n })

In this example, the variable n is exposed to the invoked action as a field of the input parameter object. Moreover, the value of the n field of the output parameter object is assigned back to variable n.

composer.if(condition, consequent[, alternate])

composer.if(condition, consequent, alternate) runs either the consequent task if the condition evaluates to true or the alternate task if not. The condition task and consequent task or alternate task are all invoked on the input parameter object for the composition. The output parameter object of the condition task is discarded.

A condition task evaluates to true if and only if it produces a JSON dictionary with a field value with the value true (i.e., JSON's true value). Other fields are ignored. Because JSON values other than dictionaries are implicitly lifted to dictionaries with a value field, condition may be a Javascript function returning a Boolean value.

An expression such as params.n > 0 is not a valid condition (or in general a valid task). One should write instead: (params) => params.n > 0.

The alternate task may be omitted.

If condition fails, neither branch is executed.

For examples, the following composition divides parameter n by two if it is even.

composer.if(({n}) => n % 2 == 0, ({n}) => ({ n: n / 2 }))

composer.while(condition, task)

composer.while(condition, task) runs task repeatedly while condition evaluates to true. The condition task is evaluated before any execution of task. See composer.if for a discussion of conditions.

A failure of condition or task interrupts the execution.

composer.try(task, handler)

composer.try(task, handler) runs task with error handler handler.

A task failure triggers the execution of handler with the error object as its input parameter object.

composer.repeat(count, task)

composer.repeat(count, task) runs task count times. It is equivalent to a sequence with count task(s).

composer.retry(count, task)

composer.retry(count, task) runs task and retries task up to count times if it fails. The output parameter object for the composition is either the output parameter object of the successful task invocation or the error object produced by the last task invocation.

composer.retain(task[, flag])

composer.retain(task[, flag]) runs task on the input parameter object producing an object with two fields params and result such that params is the input parameter object of the composition and result is the output parameter object of task.

If task fails and flag is true, then the output parameter object for the composition is the combination of the input parameters with the error object. If task fails and flag is false or absent, then the output parameter object for the composition is only the error object.