V4 (#55)

- The composition json is now _just_ the AST with no implicit lowering.
- `composer.lower(composition, [combinators])` is now exposed and gives control over the lowering (if desired).
- The `compose` command has two new options: `--lower` and `--version`. 
- The combinators are now automatically derived from spec, e.g, `if: { args: [{ _: 'test' }, { _: 'consequent' }, { _: 'alternate', optional: true }], since: '0.4.0' }`.
- The spec for the combinators is now exposed as well (`composer.combinators`).
- The core language has shrunk thanks to `composer.mask` and a more aggressive use of lowering.
- The fsm has fewer states.
- The code is now split into three components: 1 compiler component available client-side and server-side, 2 client-side composer, and 3 server-side conductor.
diff --git a/bin/compose b/bin/compose
index a34444c..fd16e8a 100755
--- a/bin/compose
+++ b/bin/compose
@@ -8,11 +8,16 @@
 const composer = require('../composer')
 
 const argv = minimist(process.argv.slice(2), {
-    string: ['apihost', 'auth', 'deploy'],
-    boolean: ['insecure', 'encode', 'json'],
-    alias: { auth: 'u', insecure: 'i' }
+    string: ['apihost', 'auth', 'deploy', 'lower'],
+    boolean: ['insecure', 'encode', 'json', 'version'],
+    alias: { auth: 'u', insecure: 'i', version: 'v' }
 })
 
+if (argv.version) {
+    console.log(composer.version)
+    return
+}
+
 let count = 0
 if (argv.json) count++
 if (argv.encode) count++
@@ -26,27 +31,29 @@
     console.error('  --deploy NAME          deploy the composition with name NAME')
     console.error('  --encode               output the conductor action code for the composition')
     console.error('Flags:')
+    console.error('  --lower [VERSION]      lower to primitive combinators or specific composer version')
     console.error('  --apihost HOST         API HOST')
     console.error('  -u, --auth KEY         authorization KEY')
     console.error('  -i, --insecure         bypass certificate checking')
+    console.error('  -v, --version          output the composer version')
     return
 }
 
 const filename = argv._[0]
 const source = fs.readFileSync(filename, { encoding: 'utf8' })
-const composition = filename.slice(filename.lastIndexOf('.')) === '.js' ? vm.runInNewContext(source, { composer, require, console, process }) : composer.deserialize(JSON.parse(source))
+let composition = filename.slice(filename.lastIndexOf('.')) === '.js' ? vm.runInNewContext(source, { composer, require, console, process }) : composer.deserialize(JSON.parse(source))
+const lower = typeof argv.lower === 'string' ? argv.lower : false
 if (argv.deploy) {
     const options = { ignore_certs: argv.insecure }
     if (argv.apihost) options.apihost = argv.apihost
     if (argv.auth) options.api_key = argv.auth
-    const obj = composition.named(argv.deploy)
-    composer.openwhisk(options).compositions.deploy(obj)
-        .then(() => {
+    composer.openwhisk(options).compositions.deploy(composer.composition(argv.deploy, composition), lower)
+        .then(obj => {
             const names = obj.actions.map(action => action.name)
             console.log(`ok: created action${names.length > 1 ? 's' : ''} ${names}`)
         }, console.error)
 } else if (argv.encode) {
-    console.log(composition.encode('anonymous').actions.slice(-1)[0].action.exec.code)
+    console.log(composer.encode(composer.composition('anonymous', composition), lower).actions.slice(-1)[0].action.exec.code)
 } else {
-    console.log(JSON.stringify(composition, null, 4))
+    console.log(JSON.stringify(composer.lower(composition, lower), null, 4))
 }
diff --git a/composer.js b/composer.js
index 24700aa..7b446a7 100644
--- a/composer.js
+++ b/composer.js
@@ -16,386 +16,522 @@
 
 'use strict'
 
-// composer module
+// compiler code shared between composer and conductor (to permit client-side and server-side lowering)
 
-const fs = require('fs')
-const os = require('os')
-const path = require('path')
-const util = require('util')
-const uglify = require('uglify-es')
-const { version } = require('./package.json')
+function compiler() {
+    const util = require('util')
+    const semver = require('semver')
 
-class ComposerError extends Error {
-    constructor(message, argument) {
-        super(message + (typeof argument !== 'undefined' ? '\nArgument: ' + util.inspect(argument) : ''))
+    // standard combinators
+    const combinators = {
+        empty: { since: '0.4.0' },
+        seq: { components: true, since: '0.4.0' },
+        sequence: { components: true, since: '0.4.0' },
+        if: { args: [{ _: 'test' }, { _: 'consequent' }, { _: 'alternate', optional: true }], since: '0.4.0' },
+        if_nosave: { args: [{ _: 'test' }, { _: 'consequent' }, { _: 'alternate', optional: true }], since: '0.4.0' },
+        while: { args: [{ _: 'test' }, { _: 'body' }], since: '0.4.0' },
+        while_nosave: { args: [{ _: 'test' }, { _: 'body' }], since: '0.4.0' },
+        dowhile: { args: [{ _: 'body' }, { _: 'test' }], since: '0.4.0' },
+        dowhile_nosave: { args: [{ _: 'body' }, { _: 'test' }], since: '0.4.0' },
+        try: { args: [{ _: 'body' }, { _: 'handler' }], since: '0.4.0' },
+        finally: { args: [{ _: 'body' }, { _: 'finalizer' }], since: '0.4.0' },
+        retain: { components: true, since: '0.4.0' },
+        retain_catch: { components: true, since: '0.4.0' },
+        let: { args: [{ _: 'declarations', type: 'object' }], components: true, since: '0.4.0' },
+        mask: { components: true, since: '0.4.0' },
+        action: { args: [{ _: 'name', type: 'string' }, { _: 'action', type: 'object', optional: true }], since: '0.4.0' },
+        composition: { args: [{ _: 'name', type: 'string' }, { _: 'composition' }], since: '0.4.0' },
+        repeat: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' },
+        retry: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' },
+        value: { args: [{ _: 'value', type: 'value' }], since: '0.4.0' },
+        literal: { args: [{ _: 'value', type: 'value' }], since: '0.4.0' },
+        function: { args: [{ _: 'function', type: 'object' }], since: '0.4.0' }
     }
-}
 
-/**
- * Validates options and converts to JSON
- */
-function validate(options) {
-    if (options == null) return
-    if (typeof options !== 'object' || Array.isArray(options)) throw new ComposerError('Invalid options', options)
-    options = JSON.stringify(options)
-    if (options === '{}') return
-    return JSON.parse(options)
-}
+    // composer error class
+    class ComposerError extends Error {
+        constructor(message, argument) {
+            super(message + (argument !== undefined ? '\nArgument: ' + util.inspect(argument) : ''))
+        }
+    }
 
-/**
- * Encodes a composition as an action by injecting the conductor code
- */
-function encode({ name, action }) {
-    if (action.exec.kind !== 'composition') return { name, action }
-    const code = `// generated by composer v${version}\n\nconst composition = ${JSON.stringify(action.exec.composition, null, 4)}\n\n// do not edit below this point\n\n${conductor}(eval,composition)` // invoke conductor on composition
-    return { name, action: { exec: { kind: 'nodejs:default', code }, annotations: [{ key: 'conductor', value: action.exec.composition }] } }
-}
+    // composition class
+    class Composition {
+        // weaker instanceof to tolerate multiple instances of this class
+        static [Symbol.hasInstance](instance) {
+            return instance.constructor && instance.constructor.name === Composition.name
+        }
 
-/**
- * Parses a (possibly fully qualified) resource name and validates it. If it's not a fully qualified name,
- * then attempts to qualify it.
- *
- * Examples string to namespace, [package/]action name
- *   foo => /_/foo
- *   pkg/foo => /_/pkg/foo
- *   /ns/foo => /ns/foo
- *   /ns/pkg/foo => /ns/pkg/foo
- */
-function parseActionName(name) {
-    if (typeof name !== 'string' || name.trim().length == 0) throw new ComposerError('Name is not specified')
-    name = name.trim()
-    let delimiter = '/'
-    let parts = name.split(delimiter)
-    let n = parts.length
-    let leadingSlash = name[0] == delimiter
-    // no more than /ns/p/a                           
-    if (n < 1 || n > 4 || (leadingSlash && n == 2) || (!leadingSlash && n == 4)) throw new ComposerError('Name is not valid')
-    // skip leading slash, all parts must be non empty (could tighten this check to match EntityName regex)
-    parts.forEach(function (part, i) { if (i > 0 && part.trim().length == 0) throw new ComposerError('Name is not valid') })
-    let newName = parts.join(delimiter)
-    if (leadingSlash) return newName
-    else if (n < 3) return `${delimiter}_${delimiter}${newName}`
-    else return `${delimiter}${newName}`
-}
+        // construct a composition object with the specified fields
+        constructor(composition) {
+            return Object.assign(this, composition)
+        }
 
-class Composition {
-    constructor(composition, options, actions = []) {
-        // collect actions defined in nested composition
-        Object.keys(composition).forEach(key => {
-            if (composition[key] instanceof Composition) {
-                // TODO: check for duplicate entries
-                actions.push(...composition[key].actions || [])
-                composition[key] = composition[key].composition
+        // apply f to all fields of type composition
+        visit(f) {
+            const combinator = combinators[this.type]
+            if (combinator.components) {
+                this.components = this.components.map(f)
             }
-        })
-        if (actions.length > 0) this.actions = actions
-        options = validate(options)
-        if (typeof options !== 'undefined') composition = Object.assign({ options }, composition)
-        // flatten composition array
-        this.composition = Array.isArray(composition) ? [].concat(...composition) : [composition]
-    }
-
-    /** Names the composition and returns a composition which invokes the named composition */
-    named(name) {
-        if (arguments.length > 1) throw new ComposerError('Too many arguments')
-        if (typeof name !== 'string') throw new ComposerError('Invalid argument', name)
-        name = parseActionName(name)
-        if (this.actions && this.actions.findIndex(action => action.name === name) !== -1) throw new ComposerError('Duplicate action name', name)
-        const actions = (this.actions || []).concat({ name, action: { exec: { kind: 'composition', composition: this.composition } } })
-        return new Composition({ type: 'action', name }, null, actions)
-    }
-
-    /** Encodes all compositions as actions by injecting the conductor code in them */
-    encode(name) {
-        if (arguments.length > 1) throw new ComposerError('Too many arguments')
-        if (typeof name !== 'undefined' && typeof name !== 'string') throw new ComposerError('Invalid argument', name)
-        const obj = typeof name === 'string' ? this.named(name) : this
-        return new Composition(obj.composition, null, obj.actions.map(encode))
-    }
-}
-
-class Compositions {
-    constructor(wsk) {
-        this.actions = wsk.actions
-    }
-
-    deploy(composition, name) {
-        if (arguments.length > 2) throw new ComposerError('Too many arguments')
-        if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
-        const obj = composition.encode(name)
-        if (obj.composition.length !== 1 || obj.composition[0].type !== 'action') throw new ComposerError('Cannot deploy anonymous composition')
-        return obj.actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { }))
-            .then(() => this.actions.update(action)), Promise.resolve())
-    }
-}
-
-class Composer {
-    openwhisk(options) {
-        // try to extract apihost and key first from whisk property file file and then from process.env
-        let apihost
-        let api_key
-
-        try {
-            const wskpropsPath = process.env.WSK_CONFIG_FILE || path.join(os.homedir(), '.wskprops')
-            const lines = fs.readFileSync(wskpropsPath, { encoding: 'utf8' }).split('\n')
-
-            for (let line of lines) {
-                let parts = line.trim().split('=')
-                if (parts.length === 2) {
-                    if (parts[0] === 'APIHOST') {
-                        apihost = parts[1]
-                    } else if (parts[0] === 'AUTH') {
-                        api_key = parts[1]
-                    }
+            for (let arg of combinator.args || []) {
+                if (arg.type === undefined) {
+                    this[arg._] = f(this[arg._], arg._)
                 }
             }
-        } catch (error) { }
-
-        if (process.env.__OW_API_HOST) apihost = process.env.__OW_API_HOST
-        if (process.env.__OW_API_KEY) api_key = process.env.__OW_API_KEY
-
-        const wsk = require('openwhisk')(Object.assign({ apihost, api_key }, options))
-        wsk.compositions = new Compositions(wsk)
-        return wsk
-    }
-
-    seq() {
-        return this.sequence(...arguments)
-    }
-
-    value() {
-        return this.literal(...arguments)
-    }
-
-    /** Takes a serialized Composition and returns a Composition instance */
-    deserialize({ composition, actions }) {
-        return new Composition(composition, null, actions)
-    }
-
-    task(obj) {
-        if (arguments.length > 1) throw new ComposerError('Too many arguments')
-        if (obj == null) return this.seq()
-        if (obj instanceof Composition) return obj
-        if (typeof obj === 'function') return this.function(obj)
-        if (typeof obj === 'string') return this.action(obj)
-        throw new ComposerError('Invalid argument', obj)
-    }
-
-    sequence() { // varargs, no options
-        return new Composition(Array.prototype.map.call(arguments, obj => this.task(obj), this))
-    }
-
-    if(test, consequent, alternate, options) {
-        if (arguments.length > 4) throw new ComposerError('Too many arguments')
-        return new Composition({ type: 'if', test: this.task(test), consequent: this.task(consequent), alternate: this.task(alternate) }, options)
-    }
-
-    while(test, body, options) {
-        if (arguments.length > 3) throw new ComposerError('Too many arguments')
-        return new Composition({ type: 'while', test: this.task(test), body: this.task(body) }, options)
-    }
-
-    dowhile(body, test, options) {
-        if (arguments.length > 3) throw new ComposerError('Too many arguments')
-        return new Composition({ type: 'dowhile', test: this.task(test), body: this.task(body) }, options)
-    }
-
-    try(body, handler, options) {
-        if (arguments.length > 3) throw new ComposerError('Too many arguments')
-        return new Composition({ type: 'try', body: this.task(body), handler: this.task(handler) }, options)
-    }
-
-    finally(body, finalizer, options) {
-        if (arguments.length > 3) throw new ComposerError('Too many arguments')
-        return new Composition({ type: 'finally', body: this.task(body), finalizer: this.task(finalizer) }, options)
-    }
-
-    let(declarations) { // varargs, no options
-        if (typeof declarations !== 'object' || declarations === null) throw new ComposerError('Invalid argument', declarations)
-        return new Composition({ type: 'let', declarations, body: this.seq(...Array.prototype.slice.call(arguments, 1)) })
-    }
-
-    literal(value, options) {
-        if (arguments.length > 2) throw new ComposerError('Too many arguments')
-        if (typeof value === 'function') throw new ComposerError('Invalid argument', value)
-        return new Composition({ type: 'literal', value: typeof value === 'undefined' ? {} : value }, options)
-    }
-
-    function(fun, options) {
-        if (arguments.length > 2) throw new ComposerError('Too many arguments')
-        if (typeof fun === 'function') {
-            fun = `${fun}`
-            if (fun.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', fun)
         }
-        if (typeof fun === 'string') {
-            fun = { kind: 'nodejs:default', code: fun }
-        }
-        if (typeof fun !== 'object' || fun === null) throw new ComposerError('Invalid argument', fun)
-        return new Composition({ type: 'function', exec: fun }, options)
     }
 
-    action(name, options) {
-        if (arguments.length > 2) throw new ComposerError('Too many arguments')
-        name = parseActionName(name) // throws ComposerError if name is not valid
-        let exec
-        if (options && Array.isArray(options.sequence)) { // native sequence
-            const components = options.sequence.map(a => a.indexOf('/') == -1 ? `/_/${a}` : a)
-            exec = { kind: 'sequence', components }
-            delete options.sequence
+    // compiler class
+    class Compiler {
+        // detect task type and create corresponding composition object
+        task(task) {
+            if (arguments.length > 1) throw new ComposerError('Too many arguments')
+            if (task === null) return this.empty()
+            if (task instanceof Composition) return task
+            if (typeof task === 'function') return this.function(task)
+            if (typeof task === 'string') return this.action(task)
+            throw new ComposerError('Invalid argument', task)
         }
-        if (options && typeof options.filename === 'string') { // read action code from file
-            options.action = fs.readFileSync(options.filename, { encoding: 'utf8' })
-            delete options.filename
-        }
-        if (options && typeof options.action === 'function') {
-            options.action = `const main = ${options.action}`
-            if (options.action.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', options.action)
-        }
-        if (options && typeof options.action === 'string') {
-            options.action = { kind: 'nodejs:default', code: options.action }
-        }
-        if (options && typeof options.action === 'object' && options.action !== null) {
-            exec = options.action
-            delete options.action
-        }
-        return new Composition({ type: 'action', name }, options, exec ? [{ name, action: { exec } }] : [])
-    }
 
-    retain(body, options) {
-        if (arguments.length > 2) throw new ComposerError('Too many arguments')
-        if (options && typeof options.filter === 'function') {
-            // return { params: filter(params), result: body(params) }
-            const filter = options.filter
-            delete options.filter
-            options.field = 'result'
-            return this.seq(this.retain(filter), this.retain(this.finally(this.function(({ params }) => params, { helper: 'retain_3' }), body), options))
+        // function combinator: stringify function code
+        function(fun) {
+            if (arguments.length > 1) throw new ComposerError('Too many arguments')
+            if (typeof fun === 'function') {
+                fun = `${fun}`
+                if (fun.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', fun)
+            }
+            if (typeof fun === 'string') {
+                fun = { kind: 'nodejs:default', code: fun }
+            }
+            if (typeof fun !== 'object' || fun === null) throw new ComposerError('Invalid argument', fun)
+            return new Composition({ type: 'function', function: { exec: fun } })
         }
-        if (options && typeof options.catch === 'boolean' && options.catch) {
-            // return { params, result: body(params) } even if result is an error
-            delete options.catch
+
+        // lowering
+
+        _empty() {
+            return this.sequence()
+        }
+
+        _seq(composition) {
+            return this.sequence(...composition.components)
+        }
+
+        _value(composition) {
+            return this._literal(composition)
+        }
+
+        _literal(composition) {
+            return this.let({ value: composition.value }, () => value)
+        }
+
+        _retain(composition) {
+            return this.let(
+                { params: null },
+                args => { params = args },
+                this.mask(...composition.components),
+                result => ({ params, result }))
+        }
+
+        _retain_catch(composition) {
             return this.seq(
-                this.retain(this.finally(body, this.function(result => ({ result }), { helper: 'retain_1' })), options),
-                this.function(({ params, result }) => ({ params, result: result.result }), { helper: 'retain_2' }))
+                this.retain(
+                    this.finally(
+                        this.seq(...composition.components),
+                        result => ({ result }))),
+                ({ params, result }) => ({ params, result: result.result }))
         }
-        if (options && typeof options.field !== 'undefined' && typeof options.field !== 'string') throw new ComposerError('Invalid options', options)
-        // return new Composition({ params, result: body(params) } if no error, otherwise body(params)
-        return new Composition({ type: 'retain', body: this.task(body) }, options)
+
+        _if(composition) {
+            return this.let(
+                { params: null },
+                args => { params = args },
+                this.if_nosave(
+                    this.mask(composition.test),
+                    this.seq(() => params, this.mask(composition.consequent)),
+                    this.seq(() => params, this.mask(composition.alternate))))
+        }
+
+        _while(composition) {
+            return this.let(
+                { params: null },
+                args => { params = args },
+                this.while_nosave(
+                    this.mask(composition.test),
+                    this.seq(() => params, this.mask(composition.body), args => { params = args })),
+                () => params)
+        }
+
+        _dowhile(composition) {
+            return this.let(
+                { params: null },
+                args => { params = args },
+                this.dowhile_nosave(
+                    this.seq(() => params, this.mask(composition.body), args => { params = args }),
+                    this.mask(composition.test)),
+                () => params)
+        }
+
+        _repeat(composition) {
+            return this.let(
+                { count: composition.count },
+                this.while(
+                    () => count-- > 0,
+                    this.mask(this.seq(...composition.components))))
+        }
+
+        _retry(composition) {
+            return this.let(
+                { count: composition.count },
+                params => ({ params }),
+                this.dowhile(
+                    this.finally(({ params }) => params, this.mask(this.retain_catch(...composition.components))),
+                    ({ result }) => result.error !== undefined && count-- > 0),
+                ({ result }) => result)
+        }
+
+        // define combinator methods for the standard combinators
+        static init() {
+            for (let type in combinators) {
+                const combinator = combinators[type]
+                // do not overwrite hand-written combinators
+                Compiler.prototype[type] = Compiler.prototype[type] || function () {
+                    const composition = new Composition({ type })
+                    const skip = combinator.args && combinator.args.length || 0
+                    if (!combinator.components && (arguments.length > skip)) {
+                        throw new ComposerError('Too many arguments')
+                    }
+                    for (let i = 0; i < skip; ++i) {
+                        const arg = combinator.args[i]
+                        const argument = arg.optional ? arguments[i] || null : arguments[i]
+                        switch (arg.type) {
+                            case undefined:
+                                composition[arg._] = this.task(argument)
+                                continue
+                            case 'value':
+                                if (typeof argument === 'function') throw new ComposerError('Invalid argument', argument)
+                                composition[arg._] = argument === undefined ? {} : argument
+                                continue
+                            case 'object':
+                                if (argument === null || Array.isArray(argument)) throw new ComposerError('Invalid argument', argument)
+                            default:
+                                if (typeof argument !== arg.type) throw new ComposerError('Invalid argument', argument)
+                                composition[arg._] = argument
+                        }
+                    }
+                    if (combinator.components) {
+                        composition.components = Array.prototype.slice.call(arguments, skip).map(obj => this.task(obj))
+                    }
+                    return composition
+                }
+            }
+        }
+
+        // return combinator list
+        get combinators() {
+            return combinators
+        }
+
+        // recursively deserialize composition
+        deserialize(composition) {
+            if (arguments.length > 1) throw new ComposerError('Too many arguments')
+            composition = new Composition(composition) // copy
+            composition.visit(composition => this.deserialize(composition))
+            return composition
+        }
+
+        // label combinators with the json path
+        label(composition) {
+            if (arguments.length > 1) throw new ComposerError('Too many arguments')
+            if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+
+            const label = path => (composition, name, array) => {
+                composition = new Composition(composition) // copy
+                composition.path = path + (name !== undefined ? (array === undefined ? `.${name}` : `[${name}]`) : '')
+                // label nested combinators
+                composition.visit(label(composition.path))
+                return composition
+            }
+
+            return label('')(composition)
+        }
+
+        // recursively label and lower combinators to the desired set of combinators (including primitive combinators)
+        lower(composition, combinators = []) {
+            if (arguments.length > 2) throw new ComposerError('Too many arguments')
+            if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+            if (!Array.isArray(combinators) && typeof combinators !== 'boolean' && typeof combinators !== 'string') throw new ComposerError('Invalid argument', combinators)
+
+            if (combinators === false) return composition // no lowering
+            if (combinators === true || combinators === '') combinators = [] // maximal lowering
+            if (typeof combinators === 'string') { // lower to combinators of specific composer version 
+                combinators = Object.keys(this.combinators).filter(key => semver.gte(combinators, this.combinators[key].since))
+            }
+
+            const lower = composition => {
+                composition = new Composition(composition) // copy
+                // repeatedly lower root combinator
+                while (combinators.indexOf(composition.type) < 0 && this[`_${composition.type}`]) {
+                    const path = composition.path
+                    composition = this[`_${composition.type}`](composition)
+                    if (path !== undefined) composition.path = path
+                }
+                // lower nested combinators
+                composition.visit(lower)
+                return composition
+            }
+
+            return lower(composition)
+        }
     }
 
-    repeat(count) { // varargs, no options
-        if (typeof count !== 'number') throw new ComposerError('Invalid argument', count)
-        return this.let({ count }, this.while(this.function(() => count-- > 0, { helper: 'repeat_1' }), this.mask(this.seq(...Array.prototype.slice.call(arguments, 1)))))
-    }
+    Compiler.init()
 
-    retry(count) { // varargs, no options
-        if (typeof count !== 'number') throw new ComposerError('Invalid argument', count)
-        const attempt = this.retain(this.seq(...Array.prototype.slice.call(arguments, 1)), { catch: true })
-        return this.let({ count },
-            this.function(params => ({ params }), { helper: 'retry_1' }),
-            this.dowhile(
-                this.finally(this.function(({ params }) => params, { helper: 'retry_2' }), this.mask(attempt)),
-                this.function(({ result }) => typeof result.error !== 'undefined' && count-- > 0, { helper: 'retry_3' })),
-            this.function(({ result }) => result, { helper: 'retry_4' }))
-    }
-
-    mask(body, options) {
-        if (arguments.length > 2) throw new ComposerError('Too many arguments')
-        return new Composition({ type: 'mask', body: this.task(body) }, options)
-    }
+    return { ComposerError, Composition, Compiler }
 }
 
-module.exports = new Composer()
+// composer module
+
+function composer() {
+    const fs = require('fs')
+    const os = require('os')
+    const path = require('path')
+    const { minify } = require('uglify-es')
+
+    // read composer version number
+    const { version } = require('./package.json')
+
+    // initialize compiler
+    const { ComposerError, Composition, Compiler } = compiler()
+
+    // capture compiler and conductor code (omitting composer code)
+    const conductorCode = minify(`const main=(${conductor})(${compiler}())`, { output: { max_line_len: 127 }, mangle: { reserved: [Composition.name] } }).code
+
+    /**
+     * Parses a (possibly fully qualified) resource name and validates it. If it's not a fully qualified name,
+     * then attempts to qualify it.
+     *
+     * Examples string to namespace, [package/]action name
+     *   foo => /_/foo
+     *   pkg/foo => /_/pkg/foo
+     *   /ns/foo => /ns/foo
+     *   /ns/pkg/foo => /ns/pkg/foo
+     */
+    function parseActionName(name) {
+        if (typeof name !== 'string' || name.trim().length == 0) throw new ComposerError('Name is not specified')
+        name = name.trim()
+        let delimiter = '/'
+        let parts = name.split(delimiter)
+        let n = parts.length
+        let leadingSlash = name[0] == delimiter
+        // no more than /ns/p/a
+        if (n < 1 || n > 4 || (leadingSlash && n == 2) || (!leadingSlash && n == 4)) throw new ComposerError('Name is not valid')
+        // skip leading slash, all parts must be non empty (could tighten this check to match EntityName regex)
+        parts.forEach(function (part, i) { if (i > 0 && part.trim().length == 0) throw new ComposerError('Name is not valid') })
+        let newName = parts.join(delimiter)
+        if (leadingSlash) return newName
+        else if (n < 3) return `${delimiter}_${delimiter}${newName}`
+        else return `${delimiter}${newName}`
+    }
+
+    // management class for compositions
+    class Compositions {
+        constructor(wsk, composer) {
+            this.actions = wsk.actions
+            this.composer = composer
+        }
+
+        deploy(composition, combinators) {
+            if (arguments.length > 2) throw new ComposerError('Too many arguments')
+            if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+            if (composition.type !== 'composition') throw new ComposerError('Cannot deploy anonymous composition')
+            const obj = this.composer.encode(composition, combinators)
+            return obj.actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { }))
+                .then(() => this.actions.update(action)), Promise.resolve())
+                .then(() => obj)
+        }
+    }
+
+    // enhanced client-side compiler
+    class Composer extends Compiler {
+        // enhanced action combinator: mangle name, capture code
+        action(name, options = {}) {
+            if (arguments.length > 2) throw new ComposerError('Too many arguments')
+            name = parseActionName(name) // throws ComposerError if name is not valid
+            let exec
+            if (Array.isArray(options.sequence)) { // native sequence
+                exec = { kind: 'sequence', components: options.sequence.map(parseActionName) }
+            }
+            if (typeof options.filename === 'string') { // read action code from file
+                exec = fs.readFileSync(options.filename, { encoding: 'utf8' })
+            }
+            if (typeof options.action === 'function') { // capture function
+                exec = `const main = ${options.action}`
+                if (exec.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', options.action)
+            }
+            if (typeof options.action === 'string' || typeof options.action === 'object' && options.action !== null && !Array.isArray(options.action)) {
+                exec = options.action
+            }
+            if (typeof exec === 'string') {
+                exec = { kind: 'nodejs:default', code: exec }
+            }
+            const composition = { type: 'action', name }
+            if (exec) composition.action = { exec }
+            return new Composition(composition)
+        }
+
+        // enhanced composition combinator: mangle name
+        composition(name, composition) {
+            if (arguments.length > 2) throw new ComposerError('Too many arguments')
+            if (typeof name !== 'string') throw new ComposerError('Invalid argument', name)
+            name = parseActionName(name)
+            return new Composition({ type: 'composition', name, composition: this.task(composition) })
+        }
+
+        // return enhanced openwhisk client capable of deploying compositions
+        openwhisk(options) {
+            // try to extract apihost and key first from whisk property file file and then from process.env
+            let apihost
+            let api_key
+
+            try {
+                const wskpropsPath = process.env.WSK_CONFIG_FILE || path.join(os.homedir(), '.wskprops')
+                const lines = fs.readFileSync(wskpropsPath, { encoding: 'utf8' }).split('\n')
+
+                for (let line of lines) {
+                    let parts = line.trim().split('=')
+                    if (parts.length === 2) {
+                        if (parts[0] === 'APIHOST') {
+                            apihost = parts[1]
+                        } else if (parts[0] === 'AUTH') {
+                            api_key = parts[1]
+                        }
+                    }
+                }
+            } catch (error) { }
+
+            if (process.env.__OW_API_HOST) apihost = process.env.__OW_API_HOST
+            if (process.env.__OW_API_KEY) api_key = process.env.__OW_API_KEY
+
+            const wsk = require('openwhisk')(Object.assign({ apihost, api_key }, options))
+            wsk.compositions = new Compositions(wsk, this)
+            return wsk
+        }
+
+        // recursively encode composition into { composition, actions } by encoding nested compositions into actions and extracting nested action definitions
+        encode(composition, combinators = []) { // lower non-primitive combinators by default
+            if (arguments.length > 2) throw new ComposerError('Too many arguments')
+            if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+
+            composition = this.lower(composition, combinators)
+
+            const actions = []
+
+            const encode = composition => {
+                composition = new Composition(composition) // copy
+                composition.visit(encode)
+                if (composition.type === 'composition') {
+                    const code = `// generated by composer v${version}\n\nconst composition = ${JSON.stringify(encode(composition.composition), null, 4)}\n\n// do not edit below this point\n\n${conductorCode}` // invoke conductor on composition
+                    composition.action = { exec: { kind: 'nodejs:default', code }, annotations: [{ key: 'conductor', value: composition.composition }, { key: 'composer', value: version }] }
+                    delete composition.composition
+                    composition.type = 'action'
+                }
+                if (composition.type === 'action' && composition.action) {
+                    actions.push({ name: composition.name, action: composition.action })
+                    delete composition.action
+                }
+                return composition
+            }
+
+            composition = encode(composition)
+            return { composition, actions }
+        }
+
+        get version() {
+            return version
+        }
+    }
+
+    return new Composer()
+}
+
+module.exports = composer()
 
 // conductor action
 
-const conductor = `const main=(${uglify.minify(`${init}`, { output: { max_line_len: 127 } }).code})`
+function conductor({ Compiler }) {
+    const compiler = new Compiler()
 
-function init(__eval__, composition) {
+    this.require = require
+
     function chain(front, back) {
         front.slice(-1)[0].next = 1
         front.push(...back)
         return front
     }
 
-    function compile(json, path = '') {
-        if (Array.isArray(json)) {
-            if (json.length === 0) return [{ type: 'pass', path }]
-            return json.map((json, index) => compile(json, path + '[' + index + ']')).reduce(chain)
-        }
-        const options = json.options || {}
+    function sequence(components) {
+        if (components.length === 0) return [{ type: 'empty' }]
+        return components.map(compile).reduce(chain)
+    }
+
+    function compile(json) {
+        const path = json.path
         switch (json.type) {
+            case 'sequence':
+                return chain([{ type: 'pass', path }], sequence(json.components))
             case 'action':
                 return [{ type: 'action', name: json.name, path }]
             case 'function':
-                return [{ type: 'function', exec: json.exec, path }]
-            case 'literal':
-                return [{ type: 'literal', value: json.value, path }]
+                return [{ type: 'function', exec: json.function.exec, path }]
             case 'finally':
-                var body = compile(json.body, path + '.body')
-                const finalizer = compile(json.finalizer, path + '.finalizer')
-                var fsm = [[{ type: 'try', path }], body, [{ type: 'exit', path }], finalizer].reduce(chain)
+                var body = compile(json.body)
+                const finalizer = compile(json.finalizer)
+                var fsm = [[{ type: 'try', path }], body, [{ type: 'exit' }], finalizer].reduce(chain)
                 fsm[0].catch = fsm.length - finalizer.length
                 return fsm
             case 'let':
-                var body = compile(json.body, path + '.body')
-                return [[{ type: 'let', let: json.declarations, path }], body, [{ type: 'exit', path }]].reduce(chain)
+                var body = sequence(json.components)
+                return [[{ type: 'let', let: json.declarations, path }], body, [{ type: 'exit' }]].reduce(chain)
             case 'mask':
-                var body = compile(json.body, path + '.body')
-                return [[{ type: 'mask', path }], body, [{ type: 'exit', path }]].reduce(chain)
-            case 'retain':
-                var body = compile(json.body, path + '.body')
-                var fsm = [[{ type: 'push', path }], body, [{ type: 'pop', collect: true, path }]].reduce(chain)
-                if (options.field) fsm[0].field = options.field
-                return fsm
+                var body = sequence(json.components)
+                return [[{ type: 'let', let: null, path }], body, [{ type: 'exit' }]].reduce(chain)
             case 'try':
-                var body = compile(json.body, path + '.body')
-                const handler = chain(compile(json.handler, path + '.handler'), [{ type: 'pass', path }])
-                var fsm = [[{ type: 'try', path }], body, [{ type: 'exit', path }]].reduce(chain)
+                var body = compile(json.body)
+                const handler = chain(compile(json.handler), [{ type: 'pass' }])
+                var fsm = [[{ type: 'try', path }], body, [{ type: 'exit' }]].reduce(chain)
                 fsm[0].catch = fsm.length
                 fsm.slice(-1)[0].next = handler.length
                 fsm.push(...handler)
                 return fsm
-            case 'if':
-                var consequent = compile(json.consequent, path + '.consequent')
-                var alternate = chain(compile(json.alternate, path + '.alternate'), [{ type: 'pass', path }])
-                if (!options.nosave) consequent = chain([{ type: 'pop', path }], consequent)
-                if (!options.nosave) alternate = chain([{ type: 'pop', path }], alternate)
-                var fsm = chain(compile(json.test, path + '.test'), [{ type: 'choice', then: 1, else: consequent.length + 1, path }])
-                if (!options.nosave) fsm = chain([{ type: 'push', path }], fsm)
+            case 'if_nosave':
+                var consequent = compile(json.consequent)
+                var alternate = chain(compile(json.alternate), [{ type: 'pass' }])
+                var fsm = [[{ type: 'pass', path }], compile(json.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain)
                 consequent.slice(-1)[0].next = alternate.length
                 fsm.push(...consequent)
                 fsm.push(...alternate)
                 return fsm
-            case 'while':
-                var consequent = compile(json.body, path + '.body')
-                var alternate = [{ type: 'pass', path }]
-                if (!options.nosave) consequent = chain([{ type: 'pop', path }], consequent)
-                if (!options.nosave) alternate = chain([{ type: 'pop', path }], alternate)
-                var fsm = chain(compile(json.test, path + '.test'), [{ type: 'choice', then: 1, else: consequent.length + 1, path }])
-                if (!options.nosave) fsm = chain([{ type: 'push', path }], fsm)
+            case 'while_nosave':
+                var consequent = compile(json.body)
+                var alternate = [{ type: 'pass' }]
+                var fsm = [[{ type: 'pass', path }], compile(json.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain)
                 consequent.slice(-1)[0].next = 1 - fsm.length - consequent.length
                 fsm.push(...consequent)
                 fsm.push(...alternate)
                 return fsm
-            case 'dowhile':
-                var test = compile(json.test, path + '.test')
-                if (!options.nosave) test = chain([{ type: 'push', path }], test)
-                var fsm = [compile(json.body, path + '.body'), test, [{ type: 'choice', then: 1, else: 2, path }]].reduce(chain)
-                if (options.nosave) {
-                    fsm.slice(-1)[0].then = 1 - fsm.length
-                    fsm.slice(-1)[0].else = 1
-                } else {
-                    fsm.push({ type: 'pop', path })
-                    fsm.slice(-1)[0].next = 1 - fsm.length
-                }
-                var alternate = [{ type: 'pass', path }]
-                if (!options.nosave) alternate = chain([{ type: 'pop', path }], alternate)
+            case 'dowhile_nosave':
+                var test = compile(json.test)
+                var fsm = [[{ type: 'pass', path }], compile(json.body), test, [{ type: 'choice', then: 1, else: 2 }]].reduce(chain)
+                fsm.slice(-1)[0].then = 1 - fsm.length
+                fsm.slice(-1)[0].else = 1
+                var alternate = [{ type: 'pass' }]
                 fsm.push(...alternate)
                 return fsm
         }
     }
 
-    const fsm = compile(composition)
+    const fsm = compile(compiler.lower(compiler.label(compiler.deserialize(composition))))
 
     const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj)
 
@@ -418,11 +554,11 @@
         let stack = []
 
         // restore state and stack when resuming
-        if (typeof params.$resume !== 'undefined') {
+        if (params.$resume !== undefined) {
             if (!isObject(params.$resume)) return badRequest('The type of optional $resume parameter must be object')
             state = params.$resume.state
             stack = params.$resume.stack
-            if (typeof state !== 'undefined' && typeof state !== 'number') return badRequest('The type of optional $resume.state parameter must be number')
+            if (state !== undefined && typeof state !== 'number') return badRequest('The type of optional $resume.state parameter must be number')
             if (!Array.isArray(stack)) return badRequest('The type of $resume.stack must be an array')
             delete params.$resume
             inspect() // handle error objects when resuming
@@ -431,7 +567,7 @@
         // wrap params if not a dictionary, branch to error handler if error
         function inspect() {
             if (!isObject(params)) params = { value: params }
-            if (typeof params.error !== 'undefined') {
+            if (params.error !== undefined) {
                 params = { error: params.error } // discard all fields but the error field
                 state = undefined // abort unless there is a handler in the stack
                 while (stack.length > 0) {
@@ -445,12 +581,12 @@
             // handle let/mask pairs
             const view = []
             let n = 0
-            for (let i in stack) {
-                if (typeof stack[i].mask !== 'undefined') {
+            for (let frame of stack) {
+                if (frame.let === null) {
                     n++
-                } else if (typeof stack[i].let !== 'undefined') {
+                } else if (frame.let !== undefined) {
                     if (n === 0) {
-                        view.push(stack[i])
+                        view.push(frame)
                     } else {
                         n--
                     }
@@ -459,8 +595,8 @@
 
             // update value of topmost matching symbol on stack if any
             function set(symbol, value) {
-                const element = view.find(element => typeof element.let !== 'undefined' && typeof element.let[symbol] !== 'undefined')
-                if (typeof element !== 'undefined') element.let[symbol] = JSON.parse(JSON.stringify(value))
+                const element = view.find(element => element.let !== undefined && element.let[symbol] !== undefined)
+                if (element !== undefined) element.let[symbol] = JSON.parse(JSON.stringify(value))
             }
 
             // collapse stack for invocation
@@ -471,7 +607,7 @@
             for (const name in env) main += `arguments[1]['${name}']=${name};`
             main += '}})'
             try {
-                return __eval__(main)(params, env)
+                return (1, eval)(main)(params, env)
             } finally {
                 for (const name in env) set(name, env[name])
             }
@@ -479,7 +615,7 @@
 
         while (true) {
             // final state, return composition result
-            if (typeof state === 'undefined') {
+            if (state === undefined) {
                 console.log(`Entering final state`)
                 console.log(JSON.stringify(params))
                 if (params.error) return params; else return { params }
@@ -487,9 +623,9 @@
 
             // process one state
             const json = fsm[state] // json definition for current state
-            console.log(`Entering state ${state} at path fsm${json.path}`)
+            if (json.path !== undefined) console.log(`Entering composition${json.path}`)
             const current = state
-            state = typeof json.next === 'undefined' ? undefined : current + json.next // default next state
+            state = json.next === undefined ? undefined : current + json.next // default next state
             switch (json.type) {
                 case 'choice':
                     state = current + (params.value ? json.then : json.else)
@@ -500,27 +636,13 @@
                 case 'let':
                     stack.unshift({ let: JSON.parse(JSON.stringify(json.let)) })
                     break
-                case 'mask':
-                    stack.unshift({ mask: true })
-                    break
                 case 'exit':
                     if (stack.length === 0) return internalError(`State ${current} attempted to pop from an empty stack`)
                     stack.shift()
                     break
-                case 'push':
-                    stack.unshift(JSON.parse(JSON.stringify({ params: json.field ? params[json.field] : params })))
-                    break
-                case 'pop':
-                    if (stack.length === 0) return internalError(`State ${current} attempted to pop from an empty stack`)
-                    params = json.collect ? { params: stack.shift().params, result: params } : stack.shift().params
-                    break
                 case 'action':
                     return { action: json.name, params, state: { $resume: { state, stack } } } // invoke continuation
                     break
-                case 'literal':
-                    params = JSON.parse(JSON.stringify(json.value))
-                    inspect()
-                    break
                 case 'function':
                     let result
                     try {
@@ -531,11 +653,13 @@
                     }
                     if (typeof result === 'function') result = { error: `State ${current} evaluated to a function` }
                     // if a function has only side effects and no return value, return params
-                    params = JSON.parse(JSON.stringify(typeof result === 'undefined' ? params : result))
+                    params = JSON.parse(JSON.stringify(result === undefined ? params : result))
+                    inspect()
+                    break
+                case 'empty':
                     inspect()
                     break
                 case 'pass':
-                    inspect()
                     break
                 default:
                     return internalError(`State ${current} has an unknown type`)
diff --git a/docs/COMBINATORS.md b/docs/COMBINATORS.md
index 81be948..c72530b 100644
--- a/docs/COMBINATORS.md
+++ b/docs/COMBINATORS.md
@@ -7,16 +7,19 @@
 | [`action`](#action) | action | `composer.action('echo')` |
 | [`function`](#function) | function | `composer.function(({ x, y }) => ({ product: x * y }))` |
 | [`literal` or `value`](#literal) | constant value | `composer.literal({ message: 'Hello, World!' })` |
+| [`composition`](#composition) | named composition | `composer.composition('myCompositionName', myComposition)` |
+| [`empty`](#empty) | empty sequence | `composer.empty()`
 | [`sequence` or `seq`](#sequence) | sequence | `composer.sequence('hello', 'bye')` |
 | [`let`](#let) | variable declarations | `composer.let({ count: 3, message: 'hello' }, ...)` |
-| [`if`](#if) | conditional | `composer.if('authenticate', 'success', 'failure')` |
-| [`while`](#while) | loop | `composer.while('notEnough', 'doMore')` |
-| [`dowhile`](#dowhile) | loop at least once | `composer.dowhile('fetchData', 'needMoreData')` |
+| [`mask`](#mask) | variable hiding | `composer.let({ n }, composer.while(_ => n-- > 0, composer.mask(composition)))` |
+| [`if` and `if_nosave`](#if) | conditional | `composer.if('authenticate', 'success', 'failure')` |
+| [`while` and `while_nosave`](#while) | loop | `composer.while('notEnough', 'doMore')` |
+| [`dowhile` and `dowhile_nosave`](#dowhile) | loop at least once | `composer.dowhile('fetchData', 'needMoreData')` |
 | [`repeat`](#repeat) | counted loop | `composer.repeat(3, 'hello')` |
 | [`try`](#try) | error handling | `composer.try('divideByN', 'NaN')` |
 | [`finally`](#finally) | finalization | `composer.finally('tryThis', 'doThatAlways')` |
 | [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` |
-| [`retain`](#retain) | persistence | `composer.retain('validateInput')` |
+| [`retain` and `retain_catch`](#retain) | persistence | `composer.retain('validateInput')` |
 
 The `action`, `function`, and `literal` combinators construct compositions respectively from actions, functions, and constant values. The other combinators combine existing compositions to produce new compositions.
 
@@ -25,7 +28,11 @@
 Where a composition is expected, the following shorthands are permitted:
  - `name` of type `string` stands for `composer.action(name)`,
  - `fun` of type `function` stands for `composer.function(fun)`,
- - `null` stands for the empty sequence `composer.sequence()`.
+ - `null` stands for the empty sequence `composer.empty()`.
+
+## Primitive combinators
+
+Some of these combinators are _derived_ combinators: they are equivalent to combinations of other combinators. The `composer` module offers a `composer.lower` method (see [COMPOSER.md](#COMPOSER.md)) that can eliminate derived combinators from a composition, producing an equivalent composition made only of _primitive_ combinators. The primitive combinators are: `action`, `function`, `composition`, `sequence`, `let`, `mask`, `if_nosave`, `while_nosave`, `dowhile_nosave`, `try`, and `finally`.
 
 ## Action
 
@@ -129,6 +136,23 @@
 
 In general, a function can be embedded in a composition either by using the `composer.function` combinator, or by embedding the source code for the function as a string and later using `eval` to evaluate the function code.
 
+## Composition
+
+`composition(name, composition)` returns a composition consisting of the invocation of the composition named `name` and of the declaration of the composition named `name` defined to be `composition`.
+
+```javascript
+composer.if('isEven', 'half', composer.composition('tripleAndIncrement', composer.sequence('triple', 'increment')))
+```
+In this example, the `composer.sequence('triple', 'increment')` composition is given the name `tripleAndIncrement` and the enclosing composition references the `tripleAndIncrement` composition by name. In particular, deploying this composition actually deploys two compositions:
+- a composition named `tripleAndIncrement` defined as `composer.sequence('triple', 'increment')`, and
+- a composition defined as `composer.if('isEven', 'half', 'tripleAndIncrement')` whose name will be specified as deployment time.
+
+Importantly, the behavior of the second composition would be altered if we redefine the `tripleAndIncrement` composition to do something else, since it refers to the composition by name.
+
+## Empty
+
+`composer.empty()` is a shorthand for the empty sequence `composer.sequence()`. It is typically used to make it clear that a composition, e.g., a branch of an `if` combinator, is intentionally doing nothing.
+
 ## Sequence
 
 `composer.sequence(composition_1, composition_2, ...)` chains a series of compositions (possibly empty).
@@ -158,30 +182,49 @@
 
 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 field `n` of the output parameter object is assigned back to variable `n`.
 
+## Mask
+
+`composer.mask(composition)` is meant to be used in combination with the `let` combinator. It makes it possible to hide the innermost enclosing `let` combinator from _composition_. It is typically used to define composition templates that need to introduce variables.
+
+For instance, the following function is a possible implementation of a repeat loop:
+```javascript
+function loop(n, composition) {
+    return .let({ n }, composer.while(() => n-- > 0, composer.mask(composition)))
+}
+```
+This function takes two parameters: the number of iterations _n_ and the _composition_ to repeat _n_ times. Here, the `mask` combinator makes sure that this declaration of _n_ is not visible to _composition_. Thanks to `mask`, the following example correctly returns `{ value: 12 }`.
+```javascript
+composer.let({ n: 0 }, loop(3, loop(4, () => ++n)))
+```
+While composer variables are dynamically scoped, the `mask` combinator alleviates the biggest concern with dynamic scoping: incidental name collision.
+
 ## If
 
-`composer.if(condition, consequent, [alternate], [options])` runs either the _consequent_ composition if the _condition_ evaluates to true or the _alternate_ composition if not.
+`composer.if(condition, consequent, [alternate])` runs either the _consequent_ composition if the _condition_ evaluates to true or the _alternate_ composition if not.
 
 A _condition_ composition evaluates to true if and only if it produces a JSON dictionary with a field `value` with value `true`. 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 composition). One should write instead `params => params.n > 0`. The input parameter object for the composition is the input parameter object for the _condition_ composition.
 
 The _alternate_ composition may be omitted. If _condition_ fails, neither branch is executed.
 
-The optional `options` dictionary supports a `nosave` option. If `options.nosave` is thruthy, the _consequent_ composition or _alternate_ composition is invoked on the output parameter object of the _condition_ composition. Otherwise, the output parameter object of the _condition_ composition is discarded and the _consequent_ composition or _alternate_ composition is invoked on the input parameter object for the composition. For example, the following compositions divide parameter `n` by two if `n` is even:
+The output parameter object of the _condition_ composition is discarded, one the choice of a branch has been made and the _consequent_ composition or _alternate_ composition is invoked on the input parameter object for the composition. For example, the following composition divides parameter `n` by two if `n` is even:
 ```javascript
 composer.if(params => params.n % 2 === 0, params => { params.n /= 2 })
-composer.if(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }, null, { nosave: true })
+```
+The `if_nosave` combinator is similar but it does not preserve the input parameter object, i.e., the _consequent_ composition or _alternate_ composition is invoked on the output parameter object of _condition_. The following example also divides parameter `n` by two if `n` is even:
+```javascript
+composer.if_nosave(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 })
 ```
 In the first example, the condition function simply returns a Boolean value. The consequent function uses the saved input parameter object to compute `n`'s value. In the second example, the condition function adds a `value` field to the input parameter object. The consequent function applies to the resulting object. In particular, in the second example, the output parameter object for the condition includes the `value` field.
 
-While, the default `nosave == false` behavior is typically more convenient, preserving the input parameter object is not free as it counts toward the parameter size limit for OpenWhisk actions. In essence, the limit on the size of parameter objects processed during the evaluation of the condition is reduced by the size of the saved parameter object. The `nosave` option omits the parameter save, hence preserving the parameter size limit.
+While, the `if` combinator is typically more convenient, preserving the input parameter object is not free as it counts toward the parameter size limit for OpenWhisk actions. In essence, the limit on the size of parameter objects processed during the evaluation of the condition is reduced by the size of the saved parameter object. The `if_nosave` combinator omits the parameter save, hence preserving the parameter size limit.
 
 ## While
 
-`composer.while(condition, body, [options])` runs _body_ repeatedly while _condition_ evaluates to true. The _condition_ composition is evaluated before any execution of the _body_ composition. See [composer.if](#composerifcondition-consequent-alternate) for a discussion of conditions.
+`composer.while(condition, body)` runs _body_ repeatedly while _condition_ evaluates to true. The _condition_ composition is evaluated before any execution of the _body_ composition. See [composer.if](#composerifcondition-consequent-alternate) for a discussion of conditions.
 
 A failure of _condition_ or _body_ interrupts the execution. The composition returns the error object from the failed component.
 
-Like `composer.if`, `composer.while` supports a `nosave` option. By default, the output parameter object of the _condition_ composition is discarded and the input parameter object for the _body_ composition is either the input parameter object for the whole composition the first time around or the output parameter object of the previous iteration of _body_. However if `options.nosave` is thruthy, the input parameter object for _body_ is the output parameter object of _condition_. Moreover, the output parameter object for the whole composition is the output parameter object of the last _condition_ evaluation.
+The output parameter object of the _condition_ composition is discarded and the input parameter object for the _body_ composition is either the input parameter object for the whole composition the first time around or the output parameter object of the previous iteration of _body_. However, if `while_nosave` combinator is used, the input parameter object for _body_ is the output parameter object of _condition_. Moreover, the output parameter object for the whole composition is the output parameter object of the last _condition_ evaluation.
 
 For instance, the following composition invoked on dictionary `{ n: 28 }` returns `{ n: 7 }`:
 ```javascript
@@ -189,12 +232,14 @@
 ```
 For instance, the following composition invoked on dictionary `{ n: 28 }` returns `{ n: 7, value: false }`:
 ```javascript
-composer.while(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }, { nosave: true })
+composer.while_nosave(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 })
 ```
 
 ## Dowhile
 
-`composer.dowhile(condition, body, [options])` is similar to `composer.while(body, condition, [options])` except that _body_ is invoked before _condition_ is evaluated, hence _body_ is always invoked at least once.
+`composer.dowhile(condition, body)` is similar to `composer.while(body, condition)` except that _body_ is invoked before _condition_ is evaluated, hence _body_ is always invoked at least once.
+
+Like `while_nosave`, `dowhile_nosave` does not implicitly preserve the parameter object while evaluating _condition_.
 
 ## Repeat
 
@@ -218,9 +263,6 @@
 
 ## Retain
 
-`composer.retain(body, [options])` runs _body_ 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 _body_.
+`composer.retain(body)` runs _body_ 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 _body_.
 
-An `options` dictionary object may be specified to alter the default behavior of `composer.retain` in the following ways:
-- If `options.catch` is thruthy, the `retain` combinator behavior will be the same even if _body_ returns an error object. Otherwise, if _body_ fails, the output of the `retain` combinator is only the error object (i.e., the input parameter object is not preserved).
-- If `options.filter` is a function, the combinator only persists the result of the function application to the input parameter object.
-- If `options.field` is a string, the combinator only persists the value of the field of the input parameter object with the given name.
+If _body_ fails, the output of the `retain` combinator is only the error object (i.e., the input parameter object is not preserved). In constrast, the `retain_catch` combinator always outputs `{ params, result }`, even if `result` is an error result.
diff --git a/docs/COMPOSE.md b/docs/COMPOSE.md
index 6b996f9..4142ab8 100644
--- a/docs/COMPOSE.md
+++ b/docs/COMPOSE.md
@@ -17,6 +17,7 @@
   --deploy NAME          deploy the composition with name NAME
   --encode               output the conductor action code for the composition
 Flags:
+  --lower [VERSION]      lower to primitive combinators or specific composer version
   --apihost HOST         API HOST
   -u, --auth KEY         authorization KEY
   -i, --insecure         bypass certificate checking
@@ -36,58 +37,37 @@
 ```
 ```json
 {
-    "actions": [
-        {
-            "name": "/_/authenticate",
-            "action": {
-                "exec": {
-                    "kind": "nodejs:default",
-                    "code": "const main = function ({ password }) { return { value: password === 'abc123' } }"
-                }
-            }
-        },
-        {
-            "name": "/_/success",
-            "action": {
-                "exec": {
-                    "kind": "nodejs:default",
-                    "code": "const main = function () { return { message: 'success' } }"
-                }
-            }
-        },
-        {
-            "name": "/_/failure",
-            "action": {
-                "exec": {
-                    "kind": "nodejs:default",
-                    "code": "const main = function () { return { message: 'failure' } }"
-                }
+    "type": "if",
+    "test": {
+        "type": "action",
+        "name": "/_/authenticate",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function ({ password }) { return { value: password === 'abc123' } }"
             }
         }
-    ],
-    "composition": [
-        {
-            "type": "if",
-            "test": [
-                {
-                    "type": "action",
-                    "name": "/_/authenticate"
-                }
-            ],
-            "consequent": [
-                {
-                    "type": "action",
-                    "name": "/_/success"
-                }
-            ],
-            "alternate": [
-                {
-                    "type": "action",
-                    "name": "/_/failure"
-                }
-            ]
+    },
+    "consequent": {
+        "type": "action",
+        "name": "/_/success",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function () { return { message: 'success' } }"
+            }
         }
-    ]
+    },
+    "alternate": {
+        "type": "action",
+        "name": "/_/failure",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function () { return { message: 'failure' } }"
+            }
+        }
+    }
 }
 ```
 The evaluation context includes the `composer` object implicitly defined as:
@@ -150,3 +130,11 @@
 ok: created action demo
 ```
 The conductor action code does not include definitions for nested actions or compositions.
+
+## Lowering
+
+If the `--lower VERSION` option is specified, the `compose` command uses the set of combinators of the specified revision of the `composer` module. More recently introduced combinators (if any) are translated into combinators of the older set.
+
+If the `--lower` option is specified without a version number, the `compose` command uses only primitive combinators.
+
+These options may be combined with any of the `compose` commands.
diff --git a/docs/COMPOSER.md b/docs/COMPOSER.md
index 1a98918..71efd2e 100644
--- a/docs/COMPOSER.md
+++ b/docs/COMPOSER.md
@@ -26,7 +26,7 @@
 // instantiate OpenWhisk client
 const wsk = composer.openwhisk({ ignore_certs: true })
 
-wsk.compositions.deploy(composition, 'demo') // deploy composition
+wsk.compositions.deploy(composer.composition('demo', composition)) // name and deploy composition
     .then(() => wsk.actions.invoke({ name: 'demo', params: { password: 'abc123' }, blocking: true })) // invoke composition
     .then(({ response }) => console.log(JSON.stringify(response.result, null, 4)), console.error)
 ```
@@ -40,15 +40,39 @@
 ```
 Alternatively, the `compose` command can deploy compositions and the OpenWhisk CLI can invoke compositions. See [COMPOSE.md](COMPOSE.md) for details.
 
-# Combinator methods
+# Composer methods
 
-The `composer` object offers a number of combinator methods to define composition objects, e.g., `composer.if`.  Combinators are documented in [COMBINATORS.md](COMBINATORS.md).
+The `composer` object offers a number of combinator methods to define composition objects, e.g., `composer.if`. Combinators are documented in [COMBINATORS.md](COMBINATORS.md). It also offers a series of helper methods described below:
+
+| Combinator | Description | Example |
+| --:| --- | --- |
+| [`deserialize`](#deserialize) | deserialization | `composer.deserialize(JSON.stringify(composition))` |
+| [`lower`](#lower) | lowering | `composer.lower(composer.if('authenticate', 'success', 'failure'), '0.4.0')` |
+| [`encode`](#encode) | code generation | `composer.encode(composition, '0.4.0')` |
+
+Finally, the `composer` object object offers an extension to the [OpenWhisk Client for Javascript](https://github.com/apache/incubator-openwhisk-client-js) that supports [deploying](#deployment) compositions.
+
+## Deserialize
+
+`composer.deserialize(composition)` recursively deserializes a serialized composition object. In other words, it recreates a `Composition` object from the input JSON dictionary.
+
+## Lower
+
+`composer.lower(composition, [combinators])` outputs a composition object equivalent to the input `composition` object but using a reduced set of combinators. The optional `combinators` parameter may specify the desired set, either directly as an array of combinator names, e.g., `['retain', 'retry']` or indirectly as a revision of the composer module, e.g., `'0.4.0'`. If the  `combinators` parameter is undefined, the set of combinators is the set of _primitive_ combinators (see [COMBINATORS.md](COMBINATORS.md])). If an array of combinators is specified the primitive combinators are implicitly added to the array. If a `composer` module revision is specified, the target combinator set is the set of combinators available as of the specified revision of the `composer` module. The `combinators` parameter may also have type Boolean. If `combinators === true` only primitive combinators are used. If `combinators === false`, there is no change to the composition.
+
+For instance, `composer.lower(composition, ['retry'])` will preserve any instance of the `retry` combinator but replace other non-primitive combinators sur as `retain`.
+
+## Encode
+
+`composer.encode(composition, [combinators])` first lowers the composition. It then converts compositions nested into `composition` into conductor actions. It finally extracts the action definitions from `composition` (both embedded action definitions and synthesized conductor actions) returning a dictionary with two fields `{ composition, actions }` where `composition` no longer contains any action or composition definitions and `actions` is the corresponding array of extracted action definitions.
+
+The optional `combinators` parameter controls the lowering. See [lower](#lower) for details.
 
 # Deployment
 
 The `composer` object offers an extension to the [OpenWhisk Client for Javascript](https://github.com/apache/incubator-openwhisk-client-js) that supports deploying compositions.
 
-## OpenWhisk method
+## Openwhisk client
 
 A client instance is obtained by invoking `composer.openwhisk([options])`, for instance with:
 ```javascript
@@ -59,9 +83,14 @@
 
 The `composer` module adds to the OpenWhisk client instance a new top-level category named `compositions` with a method named `deploy`.
 
-## Deploy method
+## Deploying compositions
 
-`wsk.deploy(composition, [name])` deploys the composition `composition`, giving name `name` to the corresponding conductor action. More precisely, it successively deploys all the actions and compositions defined in `composition` as well as `composition` itself.
+`wsk.compositions.deploy(composition, [combinators])` lowers and deploys the composition `composition`. More precisely, it successively deploys all the actions and compositions defined in `composition` including `composition` itself. The composition `composition` must have a name, hence the `deploy` method is typically used as illustrated above:
+```
+wsk.compositions.deploy(composer.composition('demo', composition))
+```
+
+The optional `combinators` parameter controls the lowering. See [lower](#lower) for details.
 
 The compositions are encoded into conductor actions prior to deployment. In other words, the `deploy` method deploys one or several actions.
 
@@ -69,9 +98,7 @@
 
 The `deploy` method deletes the deployed actions before recreating them if necessary. As a result, default parameters, limits, and annotations on preexisting actions are lost.
 
-The `name` argument may be omitted if the `composition` consists of a single action invocation. In this case, `deploy` method only deploys the actions and compositions whose definitions are nested inside `composition`.
-
-## Invoke, Update, and Delete methods
+## Invoking, updating, and deleting compositions
 
 Since compositions are deployed as conductor actions, other management tasks for compositions can be achieved by invoking methods of `wsk.actions`, for instance:
 ```javascript
diff --git a/docs/COMPOSITIONS.md b/docs/COMPOSITIONS.md
index c954bd3..4c8e416 100644
--- a/docs/COMPOSITIONS.md
+++ b/docs/COMPOSITIONS.md
@@ -13,9 +13,7 @@
 
 ## Composition objects
 
-Combinators return composition objects. Compositions object offer several helper methods described below:
-- [`composition.named(name)`](#nested-declarations) to nest one composition definition inside another,
-- [`composition.encode([name])`](#conductor-actions) to synthesize conductor actions for compositions.
+Combinators return composition objects, i.e., instances of the `Composition` class.
 
 ## Parameter objects and error objects
 
@@ -64,9 +62,9 @@
 ```
 Deploying such a composition deploys the embedded actions.
 
-A composition can also include the definition of another composition thank to the `named` method on composition objects.
+A composition can also include the definition of another composition:
 ```javascript
-composer.if('isEven', 'half', composer.sequence('triple', 'increment').named('tripleAndIncrement'))
+composer.if('isEven', 'half', composer.composition('tripleAndIncrement', composer.sequence('triple', 'increment')))
 ```
 In this example, the `composer.sequence('triple', 'increment')` composition is given the name `tripleAndIncrement` and the enclosing composition references the `tripleAndIncrement` composition by name. In other words, deploying this composition actually deploys two compositions:
 - a composition named `tripleAndIncrement` defined as `composer.sequence('triple', 'increment')`, and
@@ -79,6 +77,4 @@
 
 ## Conductor actions
 
-Compositions are implemented by means of OpenWhisk [conductor actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md). The conductor actions are implicitly synthesized when compositions are deployed using the `compose` command or the `composer` module. The `encode` method on compositions may also be used to generate the conductor actions for compositions.
-- `composition.encode()` replaces all the compositions nested inside `composition` with conductor actions. It does not alter the composition itself, only its components.
-- `composition.encode(name)` is a shorthand for `composition.named(name).encode()`. It encodes the composition and all its components into conductor actions, replacing the composition with an invocation of the action named `name` bound to the conductor action for `composition`.
+Compositions are implemented by means of OpenWhisk [conductor actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md). The conductor actions are implicitly synthesized when compositions are deployed using the `compose` command or the `composer.deploy` method. Alternatively, the `composer.encode` method can encode compositions without deploying them. See [COMPOSER.md](COMPOSER.md) for details.
\ No newline at end of file
diff --git a/docs/FORMAT.md b/docs/FORMAT.md
index dbb5793..6822db1 100644
--- a/docs/FORMAT.md
+++ b/docs/FORMAT.md
@@ -1,3 +1,60 @@
 # JSON Format
 
-This document will soon provide a specification of the JSON format for encoding compositions.
+Compositions are encoded as JSON dictionaries prior to deployment. For instance the composition in [demo.js](../samples/demo.js) is encoded as:
+```json
+{
+    "type": "if",
+    "test": {
+        "type": "action",
+        "name": "/_/authenticate",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function ({ password }) { return { value: password === 'abc123' } }"
+            }
+        }
+    },
+    "consequent": {
+        "type": "action",
+        "name": "/_/success",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function () { return { message: 'success' } }"
+            }
+        }
+    },
+    "alternate": {
+        "type": "action",
+        "name": "/_/failure",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function () { return { message: 'failure' } }"
+            }
+        }
+    }
+}
+```
+This json dictionary has one mandatory field named `type` with the name of the combinator and possible other fields that depend on the specific combinator. The values of some of these fields may be themselves composition dictionaries. In this example, the `test`, `consequent`, and `alternate` fields are compositions of `type` action.
+
+The field names and types typically match the combinator method signatures:
+
+| Type | Fields |
+| --:| --- | 
+| `action` | name:string, action:optional object |
+| `function` | function:string |
+| `literal` or `value` | value:any |
+| `composition` | name:string, composition:composition |
+| `empty` |
+| `sequence` or `seq` | components:array of compositions |
+| `let` | declarations:object, components:array of compositions |
+| `mask`| components:array of compositions |
+| `if` and `if_nosave` | test:composition, consequent:composition, alternate:composition |
+| `while` and `while_nosave` | test:composition, body:composition |
+| `dowhile` and `dowhile_nosave` | body:composition, test:composition |
+| `repeat` | count:number, components:array of compositions |
+| `try` | body:composition, handler:composition |
+| `finally` | body:composition, finalizer:composition |
+| `retry` | count:number, components:array of compositions |
+| `retain` and `retain_catch` | components:array of compositions |
diff --git a/package.json b/package.json
index e8aaca4..db49163 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
   "dependencies": {
     "minimist": "^1.2.0",
     "openwhisk": "^3.11.0",
+    "semver": "^5.3.0",
     "uglify-es": "^3.3.9"
   },
   "devDependencies": {
diff --git a/samples/demo-conductor.js b/samples/demo-conductor.js
index e8e32bf..6b6df45 100644
--- a/samples/demo-conductor.js
+++ b/samples/demo-conductor.js
@@ -1 +1,105 @@
-const main=(function init(e,t){function r(e,t){return e.slice(-1)[0].next=1,e.push(...t),e}const a=function e(t,a=""){if(Array.isArray(t))return 0===t.length?[{type:"pass",path:a}]:t.map((t,r)=>e(t,a+"["+r+"]")).reduce(r);const n=t.options||{};switch(t.type){case"action":return[{type:"action",name:t.name,path:a}];case"function":return[{type:"function",exec:t.exec,path:a}];case"literal":return[{type:"literal",value:t.value,path:a}];case"finally":var s=e(t.body,a+".body");const l=e(t.finalizer,a+".finalizer");return(o=[[{type:"try",path:a}],s,[{type:"exit",path:a}],l].reduce(r))[0].catch=o.length-l.length,o;case"let":return s=e(t.body,a+".body"),[[{type:"let",let:t.declarations,path:a}],s,[{type:"exit",path:a}]].reduce(r);case"retain":s=e(t.body,a+".body");var o=[[{type:"push",path:a}],s,[{type:"pop",collect:!0,path:a}]].reduce(r);return n.field&&(o[0].field=n.field),o;case"try":s=e(t.body,a+".body");const h=r(e(t.handler,a+".handler"),[{type:"pass",path:a}]);return(o=[[{type:"try",path:a}],s,[{type:"exit",path:a}]].reduce(r))[0].catch=o.length,o.slice(-1)[0].next=h.length,o.push(...h),o;case"if":var p=e(t.consequent,a+".consequent"),c=r(e(t.alternate,a+".alternate"),[{type:"pass",path:a}]);return n.nosave||(p=r([{type:"pop",path:a}],p)),n.nosave||(c=r([{type:"pop",path:a}],c)),o=r(e(t.test,a+".test"),[{type:"choice",then:1,else:p.length+1,path:a}]),n.nosave||(o=r([{type:"push",path:a}],o)),p.slice(-1)[0].next=c.length,o.push(...p),o.push(...c),o;case"while":return p=e(t.body,a+".body"),c=[{type:"pass",path:a}],n.nosave||(p=r([{type:"pop",path:a}],p)),n.nosave||(c=r([{type:"pop",path:a}],c)),o=r(e(t.test,a+".test"),[{type:"choice",then:1,else:p.length+1,path:a}]),n.nosave||(o=r([{type:"push",path:a}],o)),p.slice(-1)[0].next=1-o.length-p.length,o.push(...p),o.push(...c),o;case"dowhile":var i=e(t.test,a+".test");return n.nosave||(i=r([{type:"push",path:a}],i)),o=[e(t.body,a+".body"),i,[{type:"choice",then:1,else:2,path:a}]].reduce(r),n.nosave?(o.slice(-1)[0].then=1-o.length,o.slice(-1)[0].else=1):(o.push({type:"pop",path:a}),o.slice(-1)[0].next=1-o.length),c=[{type:"pass",path:a}],n.nosave||(c=r([{type:"pop",path:a}],c)),o.push(...c),o}}(t),n=e=>"object"==typeof e&&null!==e&&!Array.isArray(e),s=e=>Promise.reject({code:400,error:e}),o=e=>Promise.reject((e=>({code:"number"==typeof e.code&&e.code||500,error:"string"==typeof e.error&&e.error||e.message||"string"==typeof e&&e||"An internal error occurred"}))(e));return t=>Promise.resolve().then(()=>(function(t){let r=0,p=[];if(void 0!==t.$resume){if(!n(t.$resume))return s("The type of optional $resume parameter must be object");if(r=t.$resume.state,p=t.$resume.stack,void 0!==r&&"number"!=typeof r)return s("The type of optional $resume.state parameter must be number");if(!Array.isArray(p))return s("The type of $resume.stack must be an array");delete t.$resume,c()}function c(){if(n(t)||(t={value:t}),void 0!==t.error)for(t={error:t.error},r=void 0;p.length>0&&"number"!=typeof(r=p.shift().catch););}function i(r){function a(e,t){const r=p.find(t=>void 0!==t.let&&void 0!==t.let[e]);void 0!==r&&(r.let[e]=JSON.parse(JSON.stringify(t)))}const n=p.reduceRight((e,t)=>"object"==typeof t.let?Object.assign(e,t.let):e,{});let s="(function(){try{";for(const e in n)s+=`var ${e}=arguments[1]['${e}'];`;s+=`return eval((${r}))(arguments[0])}finally{`;for(const e in n)s+=`arguments[1]['${e}']=${e};`;s+="}})";try{return e(s)(t,n)}finally{for(const e in n)a(e,n[e])}}for(;;){if(void 0===r)return console.log("Entering final state"),console.log(JSON.stringify(t)),t.error?t:{params:t};const e=a[r];console.log(`Entering state ${r} at path fsm${e.path}`);const n=r;switch(r=void 0===e.next?void 0:n+e.next,e.type){case"choice":r=n+(t.value?e.then:e.else);break;case"try":p.unshift({catch:n+e.catch});break;case"let":p.unshift({let:JSON.parse(JSON.stringify(e.let))});break;case"exit":if(0===p.length)return o(`State ${n} attempted to pop from an empty stack`);p.shift();break;case"push":p.unshift(JSON.parse(JSON.stringify({params:e.field?t[e.field]:t})));break;case"pop":if(0===p.length)return o(`State ${n} attempted to pop from an empty stack`);t=e.collect?{params:p.shift().params,result:t}:p.shift().params;break;case"action":return{action:e.name,params:t,state:{$resume:{state:r,stack:p}}};case"literal":t=JSON.parse(JSON.stringify(e.value)),c();break;case"function":let a;try{a=i(e.exec.code)}catch(e){console.error(e),a={error:`An exception was caught at state ${n} (see log for details)`}}"function"==typeof a&&(a={error:`State ${n} evaluated to a function`}),t=JSON.parse(JSON.stringify(void 0===a?t:a)),c();break;case"pass":c();break;default:return o(`State ${n} has an unknown type`)}}})(t)).catch(o)})(eval,[{"type":"if","test":[{"type":"action","name":"/_/authenticate"}],"consequent":[{"type":"action","name":"/_/success"}],"alternate":[{"type":"action","name":"/_/failure"}]}])
+// generated by composer v0.4.0
+
+const composition = {
+    "type": "if",
+    "test": {
+        "type": "action",
+        "name": "/_/authenticate"
+    },
+    "consequent": {
+        "type": "action",
+        "name": "/_/success"
+    },
+    "alternate": {
+        "type": "action",
+        "name": "/_/failure"
+    }
+}
+
+// do not edit below this point
+
+const main=function({Compiler:e}){const t=new e;function n(e,t){return e.slice(-1)[0].next=1,e.push(...t),e}function s(e){
+return 0===e.length?[{type:"empty"}]:e.map(r).reduce(n)}function r(e){const t=e.path;switch(e.type){case"sequence":return n([{
+type:"pass",path:t}],s(e.components));case"action":return[{type:"action",name:e.name,path:t}];case"function":return[{
+type:"function",exec:e.function.exec,path:t}];case"finally":var o=r(e.body);const u=r(e.finalizer);return(c=[[{type:"try",
+path:t}],o,[{type:"exit"}],u].reduce(n))[0].catch=c.length-u.length,c;case"let":o=s(e.components);return[[{type:"let",
+let:e.declarations,path:t}],o,[{type:"exit"}]].reduce(n);case"mask":return[[{type:"let",let:null,path:t}],o=s(e.components),[{
+type:"exit"}]].reduce(n);case"try":o=r(e.body);const p=n(r(e.handler),[{type:"pass"}]);return(c=[[{type:"try",path:t}],o,[{
+type:"exit"}]].reduce(n))[0].catch=c.length,c.slice(-1)[0].next=p.length,c.push(...p),c;case"if_nosave":
+var a=r(e.consequent),i=n(r(e.alternate),[{type:"pass"}]),c=[[{type:"pass",path:t}],r(e.test),[{type:"choice",then:1,
+else:a.length+1}]].reduce(n);return a.slice(-1)[0].next=i.length,c.push(...a),c.push(...i),c;case"while_nosave":a=r(e.body),
+i=[{type:"pass"}],c=[[{type:"pass",path:t}],r(e.test),[{type:"choice",then:1,else:a.length+1}]].reduce(n)
+;return a.slice(-1)[0].next=1-c.length-a.length,c.push(...a),c.push(...i),c;case"dowhile_nosave":var l=r(e.test);(c=[[{
+type:"pass",path:t}],r(e.body),l,[{type:"choice",then:1,else:2}]].reduce(n)).slice(-1)[0].then=1-c.length,c.slice(-1)[0].else=1
+;i=[{type:"pass"}];return c.push(...i),c}}this.require=require
+;const o=r(t.lower(t.label(t.deserialize(composition)))),a=e=>"object"==typeof e&&null!==e&&!Array.isArray(e),i=e=>Promise.reject({
+code:400,error:e}),c=e=>Promise.reject((e=>({code:"number"==typeof e.code&&e.code||500,
+error:"string"==typeof e.error&&e.error||e.message||"string"==typeof e&&e||"An internal error occurred"}))(e))
+;return e=>Promise.resolve().then(()=>(function(e){let t=0,n=[];if(void 0!==e.$resume){
+if(!a(e.$resume))return i("The type of optional $resume parameter must be object");if(t=e.$resume.state,n=e.$resume.stack,
+void 0!==t&&"number"!=typeof t)return i("The type of optional $resume.state parameter must be number")
+;if(!Array.isArray(n))return i("The type of $resume.stack must be an array");delete e.$resume,s()}function s(){if(a(e)||(e={
+value:e}),void 0!==e.error)for(e={error:e.error},t=void 0;n.length>0&&"number"!=typeof(t=n.shift().catch););}function r(t){
+const s=[];let r=0;for(let e of n)null===e.let?r++:void 0!==e.let&&(0===r?s.push(e):r--);function o(e,t){
+const n=s.find(t=>void 0!==t.let&&void 0!==t.let[e]);void 0!==n&&(n.let[e]=JSON.parse(JSON.stringify(t)))}
+const a=s.reduceRight((e,t)=>"object"==typeof t.let?Object.assign(e,t.let):e,{});let i="(function(){try{"
+;for(const e in a)i+=`var ${e}=arguments[1]['${e}'];`;i+=`return eval((${t}))(arguments[0])}finally{`
+;for(const e in a)i+=`arguments[1]['${e}']=${e};`;i+="}})";try{return(0,eval)(i)(e,a)}finally{for(const e in a)o(e,a[e])}}
+for(;;){if(void 0===t)return console.log("Entering final state"),console.log(JSON.stringify(e)),e.error?e:{params:e}
+;const a=o[t];void 0!==a.path&&console.log(`Entering composition${a.path}`);const i=t;switch(t=void 0===a.next?void 0:i+a.next,
+a.type){case"choice":t=i+(e.value?a.then:a.else);break;case"try":n.unshift({catch:i+a.catch});break;case"let":n.unshift({
+let:JSON.parse(JSON.stringify(a.let))});break;case"exit":
+if(0===n.length)return c(`State ${i} attempted to pop from an empty stack`);n.shift();break;case"action":return{action:a.name,
+params:e,state:{$resume:{state:t,stack:n}}};case"function":let o;try{o=r(a.exec.code)}catch(e){console.error(e),o={
+error:`An exception was caught at state ${i} (see log for details)`}}"function"==typeof o&&(o={
+error:`State ${i} evaluated to a function`}),e=JSON.parse(JSON.stringify(void 0===o?e:o)),s();break;case"empty":s();break
+;case"pass":break;default:return c(`State ${i} has an unknown type`)}}})(e)).catch(c)}(function(){
+const e=require("util"),t=require("semver"),n={empty:{since:"0.4.0"},seq:{components:!0,since:"0.4.0"},sequence:{components:!0,
+since:"0.4.0"},if:{args:[{_:"test"},{_:"consequent"},{_:"alternate",optional:!0}],since:"0.4.0"},if_nosave:{args:[{_:"test"},{
+_:"consequent"},{_:"alternate",optional:!0}],since:"0.4.0"},while:{args:[{_:"test"},{_:"body"}],since:"0.4.0"},while_nosave:{
+args:[{_:"test"},{_:"body"}],since:"0.4.0"},dowhile:{args:[{_:"body"},{_:"test"}],since:"0.4.0"},dowhile_nosave:{args:[{
+_:"body"},{_:"test"}],since:"0.4.0"},try:{args:[{_:"body"},{_:"handler"}],since:"0.4.0"},finally:{args:[{_:"body"},{
+_:"finalizer"}],since:"0.4.0"},retain:{components:!0,since:"0.4.0"},retain_catch:{components:!0,since:"0.4.0"},let:{args:[{
+_:"declarations",type:"object"}],components:!0,since:"0.4.0"},mask:{components:!0,since:"0.4.0"},action:{args:[{_:"name",
+type:"string"},{_:"action",type:"object",optional:!0}],since:"0.4.0"},composition:{args:[{_:"name",type:"string"},{
+_:"composition"}],since:"0.4.0"},repeat:{args:[{_:"count",type:"number"}],components:!0,since:"0.4.0"},retry:{args:[{_:"count",
+type:"number"}],components:!0,since:"0.4.0"},value:{args:[{_:"value",type:"value"}],since:"0.4.0"},literal:{args:[{_:"value",
+type:"value"}],since:"0.4.0"},function:{args:[{_:"function",type:"object"}],since:"0.4.0"}};class s extends Error{
+constructor(t,n){super(t+(void 0!==n?"\nArgument: "+e.inspect(n):""))}}class Composition{static[Symbol.hasInstance](e){
+return e.constructor&&e.constructor.name===Composition.name}constructor(e){return Object.assign(this,e)}visit(e){
+const t=n[this.type];t.components&&(this.components=this.components.map(e))
+;for(let n of t.args||[])void 0===n.type&&(this[n._]=e(this[n._],n._))}}class r{task(e){
+if(arguments.length>1)throw new s("Too many arguments");if(null===e)return this.empty();if(e instanceof Composition)return e
+;if("function"==typeof e)return this.function(e);if("string"==typeof e)return this.action(e);throw new s("Invalid argument",e)}
+function(e){if(arguments.length>1)throw new s("Too many arguments")
+;if("function"==typeof e&&-1!==(e=`${e}`).indexOf("[native code]"))throw new s("Cannot capture native function",e)
+;if("string"==typeof e&&(e={kind:"nodejs:default",code:e}),"object"!=typeof e||null===e)throw new s("Invalid argument",e)
+;return new Composition({type:"function",function:{exec:e}})}_empty(){return this.sequence()}_seq(e){
+return this.sequence(...e.components)}_value(e){return this._literal(e)}_literal(e){return this.let({value:e.value},()=>value)}
+_retain(e){return this.let({params:null},e=>{params=e},this.mask(...e.components),e=>({params:params,result:e}))}
+_retain_catch(e){return this.seq(this.retain(this.finally(this.seq(...e.components),e=>({result:e}))),({params:e,result:t})=>({
+params:e,result:t.result}))}_if(e){return this.let({params:null},e=>{params=e
+},this.if_nosave(this.mask(e.test),this.seq(()=>params,this.mask(e.consequent)),this.seq(()=>params,this.mask(e.alternate))))}
+_while(e){return this.let({params:null},e=>{params=e
+},this.while_nosave(this.mask(e.test),this.seq(()=>params,this.mask(e.body),e=>{params=e})),()=>params)}_dowhile(e){
+return this.let({params:null},e=>{params=e},this.dowhile_nosave(this.seq(()=>params,this.mask(e.body),e=>{params=e
+}),this.mask(e.test)),()=>params)}_repeat(e){return this.let({count:e.count
+},this.while(()=>count-- >0,this.mask(this.seq(...e.components))))}_retry(e){return this.let({count:e.count},e=>({params:e
+}),this.dowhile(this.finally(({params:e})=>e,this.mask(this.retain_catch(...e.components))),({result:e})=>void 0!==e.error&&count-- >0),({result:e})=>e)
+}static init(){for(let e in n){const t=n[e];r.prototype[e]=r.prototype[e]||function(){const n=new Composition({type:e
+}),r=t.args&&t.args.length||0;if(!t.components&&arguments.length>r)throw new s("Too many arguments");for(let e=0;e<r;++e){
+const r=t.args[e],o=r.optional?arguments[e]||null:arguments[e];switch(r.type){case void 0:n[r._]=this.task(o);continue
+;case"value":if("function"==typeof o)throw new s("Invalid argument",o);n[r._]=void 0===o?{}:o;continue;case"object":
+if(null===o||Array.isArray(o))throw new s("Invalid argument",o);default:if(typeof o!==r.type)throw new s("Invalid argument",o)
+;n[r._]=o}}return t.components&&(n.components=Array.prototype.slice.call(arguments,r).map(e=>this.task(e))),n}}}
+get combinators(){return n}deserialize(e){if(arguments.length>1)throw new s("Too many arguments")
+;return(e=new Composition(e)).visit(e=>this.deserialize(e)),e}label(e){if(arguments.length>1)throw new s("Too many arguments")
+;if(!(e instanceof Composition))throw new s("Invalid argument",e)
+;const t=e=>(n,s,r)=>((n=new Composition(n)).path=e+(void 0!==s?void 0===r?`.${s}`:`[${s}]`:""),n.visit(t(n.path)),n)
+;return t("")(e)}lower(e,n=[]){if(arguments.length>2)throw new s("Too many arguments")
+;if(!(e instanceof Composition))throw new s("Invalid argument",e)
+;if(!Array.isArray(n)&&"boolean"!=typeof n&&"string"!=typeof n)throw new s("Invalid argument",n);if(!1===n)return e
+;!0!==n&&""!==n||(n=[]),"string"==typeof n&&(n=Object.keys(this.combinators).filter(e=>t.gte(n,this.combinators[e].since)))
+;const r=e=>{for(e=new Composition(e);n.indexOf(e.type)<0&&this[`_${e.type}`];){const t=e.path;e=this[`_${e.type}`](e),
+void 0!==t&&(e.path=t)}return e.visit(r),e};return r(e)}}return r.init(),{ComposerError:s,Composition:Composition,Compiler:r}
+}());
diff --git a/samples/demo.json b/samples/demo.json
index 1aefb82..1ecf1d8 100644
--- a/samples/demo.json
+++ b/samples/demo.json
@@ -1,54 +1,33 @@
 {
-    "actions": [
-        {
-            "name": "/_/authenticate",
-            "action": {
-                "exec": {
-                    "kind": "nodejs:default",
-                    "code": "const main = function ({ password }) { return { value: password === 'abc123' } }"
-                }
-            }
-        },
-        {
-            "name": "/_/success",
-            "action": {
-                "exec": {
-                    "kind": "nodejs:default",
-                    "code": "const main = function () { return { message: 'success' } }"
-                }
-            }
-        },
-        {
-            "name": "/_/failure",
-            "action": {
-                "exec": {
-                    "kind": "nodejs:default",
-                    "code": "const main = function () { return { message: 'failure' } }"
-                }
+    "type": "if",
+    "test": {
+        "type": "action",
+        "name": "/_/authenticate",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function ({ password }) { return { value: password === 'abc123' } }"
             }
         }
-    ],
-    "composition": [
-        {
-            "type": "if",
-            "test": [
-                {
-                    "type": "action",
-                    "name": "/_/authenticate"
-                }
-            ],
-            "consequent": [
-                {
-                    "type": "action",
-                    "name": "/_/success"
-                }
-            ],
-            "alternate": [
-                {
-                    "type": "action",
-                    "name": "/_/failure"
-                }
-            ]
+    },
+    "consequent": {
+        "type": "action",
+        "name": "/_/success",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function () { return { message: 'success' } }"
+            }
         }
-    ]
+    },
+    "alternate": {
+        "type": "action",
+        "name": "/_/failure",
+        "action": {
+            "exec": {
+                "kind": "nodejs:default",
+                "code": "const main = function () { return { message: 'failure' } }"
+            }
+        }
+    }
 }
diff --git a/samples/node-demo.js b/samples/node-demo.js
index d36c60b..5956715 100644
--- a/samples/node-demo.js
+++ b/samples/node-demo.js
@@ -26,6 +26,6 @@
 // instantiate OpenWhisk client
 const wsk = composer.openwhisk({ ignore_certs: true })
 
-wsk.compositions.deploy(composition, 'demo') // deploy composition
+wsk.compositions.deploy(composer.composition('demo', composition)) // deploy composition
     .then(() => wsk.actions.invoke({ name: 'demo', params: { password: 'abc123' }, blocking: true })) // invoke composition
     .then(({ response }) => console.log(JSON.stringify(response.result, null, 4)), console.error)
diff --git a/test/test.js b/test/test.js
index 2c20494..eed442f 100644
--- a/test/test.js
+++ b/test/test.js
@@ -7,7 +7,7 @@
 const define = action => wsk.actions.delete(action.name).catch(() => { }).then(() => wsk.actions.create(action))
 
 // deploy and invoke composition
-const invoke = (task, params = {}, blocking = true) => wsk.compositions.deploy(task, name).then(() => wsk.actions.invoke({ name, params, blocking }))
+const invoke = (task, params = {}, blocking = true) => wsk.compositions.deploy(composer.composition(name, task)).then(() => wsk.actions.invoke({ name, params, blocking }))
 
 describe('composer', function () {
     this.timeout(60000)
@@ -20,6 +20,7 @@
             .then(() => define({ name: 'isEven', action: 'function main({n}) { return { value: n % 2 == 0 } }' }))
     })
 
+
     describe('blocking invocations', function () {
         describe('actions', function () {
             it('action must return true', function () {
@@ -50,7 +51,7 @@
                 combos.forEach(({ n, s, e }) => {
                     if (s) {
                         // good cases
-                        assert.ok(composer.action(n).composition[0].name, s)
+                        assert.ok(composer.action(n).name, s)
                     } else {
                         // error cases
                         try {
@@ -63,15 +64,6 @@
                 })
             })
 
-            it('invalid options', function () {
-                try {
-                    invoke(composer.function('foo', 'foo'))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid options'))
-                }
-            })
-
             it('invalid argument', function () {
                 try {
                     invoke(composer.function(42))
@@ -83,7 +75,7 @@
 
             it('too many arguments', function () {
                 try {
-                    invoke(composer.function('foo', {}, 'foo'))
+                    invoke(composer.function('foo', 'foo'))
                     assert.fail()
                 } catch (error) {
                     assert.ok(error.message.startsWith('Too many arguments'))
@@ -100,15 +92,6 @@
                 return invoke(composer.literal(42)).then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
             })
 
-            it('invalid options', function () {
-                try {
-                    invoke(composer.literal('foo', 'foo'))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid options'))
-                }
-            })
-
             it('invalid argument', function () {
                 try {
                     invoke(composer.literal(invoke))
@@ -120,7 +103,7 @@
 
             it('too many arguments', function () {
                 try {
-                    invoke(composer.literal('foo', {}, 'foo'))
+                    invoke(composer.literal('foo', 'foo'))
                     assert.fail()
                 } catch (error) {
                     assert.ok(error.message.startsWith('Too many arguments'))
@@ -153,15 +136,6 @@
                 return invoke(composer.function('({ n }) => n % 2 === 0'), { n: 4 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
             })
 
-            it('invalid options', function () {
-                try {
-                    invoke(composer.function(() => n, 'foo'))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid options'))
-                }
-            })
-
             it('invalid argument', function () {
                 try {
                     invoke(composer.function(42))
@@ -173,7 +147,7 @@
 
             it('too many arguments', function () {
                 try {
-                    invoke(composer.function(() => n, {}, () => { }))
+                    invoke(composer.function(() => n, () => { }))
                     assert.fail()
                 } catch (error) {
                     assert.ok(error.message.startsWith('Too many arguments'))
@@ -184,7 +158,8 @@
         describe('deserialize', function () {
             it('should deserialize a serialized composition', function () {
                 const json = {
-                    "composition": [{
+                    "type": "sequence",
+                    "components": [{
                         "type": "action",
                         "name": "echo"
                     }, {
@@ -211,11 +186,11 @@
 
             describe('null task', function () {
                 it('null task must return input', function () {
-                    return invoke(composer.task(), { foo: 'foo' }).then(activation => assert.deepEqual(activation.response.result, { foo: 'foo' }))
+                    return invoke(composer.task(null), { foo: 'foo' }).then(activation => assert.deepEqual(activation.response.result, { foo: 'foo' }))
                 })
 
                 it('null task must fail on error input', function () {
-                    return invoke(composer.task(), { error: 'foo' }).then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result, { error: 'foo' }))
+                    return invoke(composer.task(null), { error: 'foo' }).then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result, { error: 'foo' }))
                 })
             })
 
@@ -303,27 +278,18 @@
                 })
 
                 it('condition = true, nosave option', function () {
-                    return invoke(composer.if('isEven', params => { params.then = true }, params => { params.else = true }, { nosave: true }), { n: 2 })
+                    return invoke(composer.if_nosave('isEven', params => { params.then = true }, params => { params.else = true }), { n: 2 })
                         .then(activation => assert.deepEqual(activation.response.result, { value: true, then: true }))
                 })
 
                 it('condition = false, nosave option', function () {
-                    return invoke(composer.if('isEven', params => { params.then = true }, params => { params.else = true }, { nosave: true }), { n: 3 })
+                    return invoke(composer.if_nosave('isEven', params => { params.then = true }, params => { params.else = true }), { n: 3 })
                         .then(activation => assert.deepEqual(activation.response.result, { value: false, else: true }))
                 })
 
-                it('invalid options', function () {
-                    try {
-                        invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement', 'TripleAndIncrement'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid options'))
-                    }
-                })
-
                 it('too many arguments', function () {
                     try {
-                        invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement', {}, 'TripleAndIncrement'))
+                        invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement', 'TripleAndIncrement'))
                         assert.fail()
                     } catch (error) {
                         assert.ok(error.message.startsWith('Too many arguments'))
@@ -343,22 +309,13 @@
                 })
 
                 it('nosave option', function () {
-                    return invoke(composer.while(({ n }) => ({ n, value: n !== 1 }), ({ n }) => ({ n: n - 1 }), { nosave: true }), { n: 4 })
+                    return invoke(composer.while_nosave(({ n }) => ({ n, value: n !== 1 }), ({ n }) => ({ n: n - 1 })), { n: 4 })
                         .then(activation => assert.deepEqual(activation.response.result, { value: false, n: 1 }))
                 })
 
-                it('invalid options', function () {
-                    try {
-                        invoke(composer.while('isNotOne', ({ n }) => ({ n: n - 1 }), ({ n }) => ({ n: n - 1 })), { n: 4 })
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid options'))
-                    }
-                })
-
                 it('too many arguments', function () {
                     try {
-                        invoke(composer.while('isNotOne', ({ n }) => ({ n: n - 1 }), {}, ({ n }) => ({ n: n - 1 })), { n: 4 })
+                        invoke(composer.while('isNotOne', ({ n }) => ({ n: n - 1 }), ({ n }) => ({ n: n - 1 })), { n: 4 })
                         assert.fail()
                     } catch (error) {
                         assert.ok(error.message.startsWith('Too many arguments'))
@@ -378,22 +335,13 @@
                 })
 
                 it('nosave option', function () {
-                    return invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), ({ n }) => ({ n, value: n !== 1 }), { nosave: true }), { n: 4 })
+                    return invoke(composer.dowhile_nosave(({ n }) => ({ n: n - 1 }), ({ n }) => ({ n, value: n !== 1 })), { n: 4 })
                         .then(activation => assert.deepEqual(activation.response.result, { value: false, n: 1 }))
                 })
 
-                it('invalid options', function () {
-                    try {
-                        invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), 'isNotOne', ({ n }) => ({ n: n - 1 })), { n: 4 })
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid options'))
-                    }
-                })
-
                 it('too many arguments', function () {
                     try {
-                        invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), 'isNotOne', {}, ({ n }) => ({ n: n - 1 })), { n: 4 })
+                        invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), 'isNotOne', ({ n }) => ({ n: n - 1 })), { n: 4 })
                         assert.fail()
                     } catch (error) {
                         assert.ok(error.message.startsWith('Too many arguments'))
@@ -413,37 +361,28 @@
                 })
 
                 it('try must throw', function () {
-                    return invoke(composer.try(composer.try(), error => ({ message: error.error })), { error: 'foo' })
+                    return invoke(composer.try(composer.task(null), error => ({ message: error.error })), { error: 'foo' })
                         .then(activation => assert.deepEqual(activation.response.result, { message: 'foo' }))
                 })
 
                 it('while must throw', function () {
-                    return invoke(composer.try(composer.while(composer.literal(false)), error => ({ message: error.error })), { error: 'foo' })
+                    return invoke(composer.try(composer.while(composer.literal(false), null), error => ({ message: error.error })), { error: 'foo' })
                         .then(activation => assert.deepEqual(activation.response.result, { message: 'foo' }))
                 })
 
                 it('if must throw', function () {
-                    return invoke(composer.try(composer.if(composer.literal(false)), error => ({ message: error.error })), { error: 'foo' })
+                    return invoke(composer.try(composer.if(composer.literal(false), null), error => ({ message: error.error })), { error: 'foo' })
                         .then(activation => assert.deepEqual(activation.response.result, { message: 'foo' }))
                 })
 
                 it('retain', function () {
-                    return invoke(composer.retain(composer.try(() => ({ p: 4 }))), { n: 3 })
+                    return invoke(composer.retain(composer.try(() => ({ p: 4 }), null)), { n: 3 })
                         .then(activation => assert.deepEqual(activation.response.result, { params: { n: 3 }, result: { p: 4 } }))
                 })
 
-                it('invalid options', function () {
-                    try {
-                        invoke(composer.try('isNotOne', 'isNotOne', 'isNotOne'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid options'))
-                    }
-                })
-
                 it('too many arguments', function () {
                     try {
-                        invoke(composer.try('isNotOne', 'isNotOne', {}, 'isNotOne'))
+                        invoke(composer.try('isNotOne', 'isNotOne', 'isNotOne'))
                         assert.fail()
                     } catch (error) {
                         assert.ok(error.message.startsWith('Too many arguments'))
@@ -462,18 +401,9 @@
                         .then(activation => assert.deepEqual(activation.response.result, { params: { error: 'foo' } }))
                 })
 
-                it('invalid options', function () {
-                    try {
-                        invoke(composer.finally('isNotOne', 'isNotOne', 'isNotOne'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid options'))
-                    }
-                })
-
                 it('too many arguments', function () {
                     try {
-                        invoke(composer.finally('isNotOne', 'isNotOne', {}, 'isNotOne'))
+                        invoke(composer.finally('isNotOne', 'isNotOne', 'isNotOne'))
                         assert.fail()
                     } catch (error) {
                         assert.ok(error.message.startsWith('Too many arguments'))
@@ -565,57 +495,9 @@
                 })
 
                 it('catch error', function () {
-                    return invoke(composer.retain(() => ({ error: 'foo' }), { catch: true }), { n: 3 })
+                    return invoke(composer.retain_catch(() => ({ error: 'foo' })), { n: 3 })
                         .then(activation => assert.deepEqual(activation.response.result, { params: { n: 3 }, result: { error: 'foo' } }))
                 })
-
-                it('select field', function () {
-                    return invoke(composer.retain('TripleAndIncrement', { field: 'p' }), { n: 3, p: 4 })
-                        .then(activation => assert.deepEqual(activation.response.result, { params: 4, result: { n: 10 } }))
-                })
-
-                it('select field, throw error', function () {
-                    return invoke(composer.retain(() => ({ error: 'foo' }), { field: 'p' }), { n: 3, p: 4 })
-                        .then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result, { error: 'foo' }))
-                })
-
-                it('select field, catch error', function () {
-                    return invoke(composer.retain(() => ({ error: 'foo' }), { field: 'p', catch: true }), { n: 3, p: 4 })
-                        .then(activation => assert.deepEqual(activation.response.result, { params: 4, result: { error: 'foo' } }))
-                })
-
-                it('filter function', function () {
-                    return invoke(composer.retain('TripleAndIncrement', { filter: ({ n }) => ({ n: -n }) }), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { params: { n: -3 }, result: { n: 10 } }))
-                })
-
-                it('filter function, throw error', function () {
-                    return invoke(composer.retain(() => ({ error: 'foo' }), { filter: ({ n }) => ({ n: -n }) }), { n: 3 })
-                        .then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result, { error: 'foo' }))
-                })
-
-                it('filter function, catch error', function () {
-                    return invoke(composer.retain(() => ({ error: 'foo' }), { filter: ({ n }) => ({ n: -n }), catch: true }), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { params: { n: - 3 }, result: { error: 'foo' } }))
-                })
-
-                it('invalid options', function () {
-                    try {
-                        invoke(composer.retain('isNotOne', 'isNotOne'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid options'))
-                    }
-                })
-
-                it('too many arguments', function () {
-                    try {
-                        invoke(composer.retain('isNotOne', {}, 'isNotOne'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Too many arguments'))
-                    }
-                })
             })
 
             describe('repeat', function () {