v0.6 dev branch (#60)

- composition files must now be modules (include require and exports statements)
- add composer.async combinator
- remove composer.composition combinator
- bug fixes
diff --git a/README.md b/README.md
index e2b66cf..7ca67d1 100644
--- a/README.md
+++ b/README.md
@@ -46,12 +46,14 @@
 Composer is distributed as Node.js package. To install this package, use the
 Node Package Manager:
 ```
+npm install @ibm-functions/composer
+```
+We recommend to also install the package globally (with `-g` option) if you intend to
+use the `compose` command to define and deploy compositions.
+```
 npm -g install @ibm-functions/composer
 ```
-We recommend to install the package globally (with `-g` option) if you intend to
-use the `compose` command to define and deploy compositions. Use a local install
-(without `-g` option) if you intend to use `node` instead. The two installations
-can coexist. Shell embeds the Composer package, so there is no need to install
+Shell embeds the Composer package, so there is no need to install
 Composer explicitly when using Shell.
 
 ## Defining a composition
@@ -59,7 +61,9 @@
 A composition is typically defined by means of a Javascript expression as
 illustrated in [samples/demo.js](samples/demo.js):
 ```javascript
-composer.if(
+const composer = require('@ibm-functions/composer')
+
+module.exports = composer.if(
     composer.action('authenticate', { action: function ({ password }) { return { value: password === 'abc123' } } }),
     composer.action('success', { action: function () { return { message: 'success' } } }),
     composer.action('failure', { action: function () { return { message: 'failure' } } }))
diff --git a/bin/compose b/bin/compose
index 34d6d6a..b2b5246 100755
--- a/bin/compose
+++ b/bin/compose
@@ -2,19 +2,18 @@
 
 'use strict'
 
-const fs = require('fs')
-const vm = require('vm')
 const minimist = require('minimist')
-let composer = require('../composer')
+const path = require('path')
 
 const argv = minimist(process.argv.slice(2), {
-    string: ['apihost', 'auth', 'deploy', 'lower', 'entity', 'entities', 'string'],
+    string: ['apihost', 'auth', 'deploy', 'lower', 'entity', 'entities', 'composer'],
     boolean: ['insecure', 'encode', 'json', 'version', 'quiet'],
     alias: { auth: 'u', insecure: 'i', version: 'v' }
 })
+const { util } = require(argv.composer || '../composer')
 
 if (argv.version) {
-    console.log(composer.version)
+    console.log(util.version)
     return
 }
 
@@ -40,31 +39,17 @@
     console.error('  -i, --insecure         bypass certificate checking')
     console.error('  -v, --version          output the composer version')
     console.error('  --quiet                omit detailed diagnostic messages')
-    console.error('  --plugin PLUGIN        register a composer plugin')
+    console.error('  --composer COMPOSER    instantiate a custom composer module')
     process.exit(127)
 }
 
 try {
-    switch (typeof argv.plugin) {
-        case 'string':
-            composer = composer.register(require(argv.plugin))
-            break
-        case 'object':
-            for (let plugin of argv.plugin) composer = composer.register(require(plugin))
-    }
     const filename = argv._[0]
-    let source
-    try {
-        source = fs.readFileSync(filename, { encoding: 'utf8' })
-    } catch (error) {
-        console.error('File not found')
-        if (!argv.quiet) console.log(error)
-        process.exit(6)
-    }
     let composition
     try {
-        composition = filename.slice(filename.lastIndexOf('.')) === '.js' ? vm.runInNewContext(source, { composer, require, console, process }) : composer.deserialize(JSON.parse(source))
-        if (typeof argv.lower === 'string') composition = composer.lower(composition, argv.lower)
+        composition = require(path.resolve(filename))
+        if (filename.slice(filename.lastIndexOf('.')) === '.json') composition = util.deserialize(composition)
+        if (typeof argv.lower === 'string') composition = util.lower(composition, argv.lower || [])
     } catch (error) {
         console.error('Bad composition')
         if (!argv.quiet) console.log(error)
@@ -75,9 +60,9 @@
         if (argv.apihost) options.apihost = argv.apihost
         if (argv.auth) options.api_key = argv.auth
         return Promise.resolve()
-            .then(() => composer.openwhisk(options).compositions.deploy(composer.composition(argv.deploy, composition), false))
-            .then(obj => {
-                const names = obj.actions.map(action => action.name)
+            .then(() => util.openwhisk(options).compositions.deploy(argv.deploy, composition))
+            .then(actions => {
+                const names = actions.map(action => action.name)
                 console.log(`ok: created action${names.length > 1 ? 's' : ''} ${names}`)
             })
             .catch(error => {
@@ -98,11 +83,11 @@
                 process.exit(5)
             })
     } else if (argv.encode) {
-        console.log(composer.encode(composer.composition('anonymous', composition), false).actions.slice(-1)[0].action.exec.code)
+        console.log(util.encode('noname', composition).slice(-1)[0].action.exec.code)
     } else if (argv.entity) {
-        console.log(JSON.stringify(composer.encode(composer.composition(argv.entity, composition), false).actions.slice(-1)[0], null, 4))
+        console.log(JSON.stringify(util.encode(argv.entity, composition).slice(-1)[0], null, 4))
     } else if (argv.entities) {
-        console.log(JSON.stringify(composer.encode(composer.composition(argv.entities, composition), false).actions, null, 4))
+        console.log(JSON.stringify(util.encode(argv.entities, composition), null, 4))
     } else {
         console.log(JSON.stringify(composition, null, 4))
     }
diff --git a/composer.js b/composer.js
index 6604d73..2dcdca3 100644
--- a/composer.js
+++ b/composer.js
@@ -14,40 +14,22 @@
  * limitations under the License.
  */
 
-'use strict'
-
 function main() {
+    'use strict'
+
     const fs = require('fs')
-    const util = require('util')
+    const os = require('os')
+    const path = require('path')
     const semver = require('semver')
+    const util = require('util')
+
+    // read composer version number
+    const version = require('./package.json').version
 
     const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj)
 
-    // default 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' }, { _: 'options', type: 'object', optional: true }], since: '0.4.0' },
-        composition: { args: [{ _: 'name', type: 'string' }, { _: 'composition' }, { _: 'options', type: 'object', optional: true }], 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' },
-    }
+    // combinator signatures
+    const combinators = {}
 
     // error class
     class ComposerError extends Error {
@@ -56,44 +38,18 @@
         }
     }
 
-    // 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
-        }
-
-        // construct a composition object with the specified fields
-        constructor(composition) {
-            return Object.assign(this, composition)
-        }
-
-        // apply f to all fields of type composition
-        visit(combinators, f) {
-            const combinator = combinators[this.type]
-            if (combinator.components) {
-                this.components = this.components.map(f)
-            }
-            for (let arg of combinator.args || []) {
-                if (arg.type === undefined) {
-                    this[arg._] = f(this[arg._], arg._)
-                }
-            }
-        }
-    }
-
     // registered plugins
     const plugins = []
 
-    // composer & lowerer
-    const composer = {
+    const composer = {}
+    Object.assign(composer, {
         // 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 === null) return composer.empty()
             if (task instanceof Composition) return task
-            if (typeof task === 'function') return this.function(task)
-            if (typeof task === 'string') return this.action(task)
+            if (typeof task === 'function') return composer.function(task)
+            if (typeof task === 'string') return composer.action(task)
             throw new ComposerError('Invalid argument', task)
         },
 
@@ -111,23 +67,20 @@
             return new Composition({ type: 'function', function: { exec: fun } })
         },
 
-        // enhanced action combinator: mangle name, capture code
+        // action combinator
         action(name, options = {}) {
             if (arguments.length > 2) throw new ComposerError('Too many arguments')
             if (!isObject(options)) throw new ComposerError('Invalid argument', options)
-            name = parseActionName(name) // throws ComposerError if name is not valid
+            name = composer.util.canonical(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 = { kind: 'sequence', components: options.sequence.map(canonical) }
+            } else if (typeof options.filename === 'string') { // read action code from file
                 exec = fs.readFileSync(options.filename, { encoding: 'utf8' })
-            }
-            if (typeof options.action === 'function') { // capture function
+            } else 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' || isObject(options.action)) {
+            } else if (typeof options.action === 'string' || isObject(options.action)) {
                 exec = options.action
             }
             if (typeof exec === 'string') {
@@ -135,150 +88,166 @@
             }
             const composition = { type: 'action', name }
             if (exec) composition.action = { exec }
-            if (options.async) composition.async = true
             return new Composition(composition)
         },
+    })
 
-        // enhanced composition combinator: mangle name
-        composition(name, composition, options = {}) {
-            if (arguments.length > 3) throw new ComposerError('Too many arguments')
-            if (!isObject(options)) throw new ComposerError('Invalid argument', options)
-            name = parseActionName(name)
-            const obj = { type: 'composition', name, composition: this.task(composition) }
-            if (options.async) obj.async = true
-            return new Composition(obj)
+    const lowerer = {
+        empty() {
+            return composer.sequence()
         },
 
-        // lowering
-
-        _empty() {
-            return this.sequence()
+        seq({ components }) {
+            return composer.sequence(...components)
         },
 
-        _seq(composition) {
-            return this.sequence(...composition.components)
+        value({ value }) {
+            return composer.literal(value)
         },
 
-        _value(composition) {
-            return this._literal(composition)
+        literal({ value }) {
+            return composer.let({ value }, composer.function('() => value'))
         },
 
-        _literal(composition) {
-            return this.let({ value: composition.value }, () => value)
-        },
-
-        _retain(composition) {
-            return this.let(
+        retain({ components }) {
+            return composer.let(
                 { params: null },
-                args => { params = args },
-                this.mask(...composition.components),
-                result => ({ params, result }))
+                composer.finally(
+                    args => { params = args },
+                    composer.seq(composer.mask(...components),
+                        result => ({ params, result }))))
         },
 
-        _retain_catch(composition) {
-            return this.seq(
-                this.retain(
-                    this.finally(
-                        this.seq(...composition.components),
+        retain_catch({ components }) {
+            return composer.seq(
+                composer.retain(
+                    composer.finally(
+                        composer.seq(...components),
                         result => ({ result }))),
                 ({ params, result }) => ({ params, result: result.result }))
         },
 
-        _if(composition) {
-            return this.let(
+        if({ test, consequent, alternate }) {
+            return composer.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))))
+                composer.finally(
+                    args => { params = args },
+                    composer.if_nosave(
+                        composer.mask(test),
+                        composer.finally(() => params, composer.mask(consequent)),
+                        composer.finally(() => params, composer.mask(alternate)))))
         },
 
-        _while(composition) {
-            return this.let(
+        while({ test, body }) {
+            return composer.let(
                 { params: null },
-                args => { params = args },
-                this.while_nosave(
-                    this.mask(composition.test),
-                    this.seq(() => params, this.mask(composition.body), args => { params = args })),
-                () => params)
+                composer.finally(
+                    args => { params = args },
+                    composer.seq(composer.while_nosave(
+                        composer.mask(test),
+                        composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args }))),
+                        () => params)))
         },
 
-        _dowhile(composition) {
-            return this.let(
+        dowhile({ body, test }) {
+            return composer.let(
                 { params: null },
-                args => { params = args },
-                this.dowhile_nosave(
-                    this.seq(() => params, this.mask(composition.body), args => { params = args }),
-                    this.mask(composition.test)),
-                () => params)
+                composer.finally(
+                    args => { params = args },
+                    composer.seq(composer.dowhile_nosave(
+                        composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args })),
+                        composer.mask(test)),
+                        () => params)))
         },
 
-        _repeat(composition) {
-            return this.let(
-                { count: composition.count },
-                this.while(
-                    () => count-- > 0,
-                    this.mask(this.seq(...composition.components))))
+        repeat({ count, components }) {
+            return composer.let(
+                { count },
+                composer.while(
+                    composer.function('() => count-- > 0'),
+                    composer.mask(...components)))
         },
 
-        _retry(composition) {
-            return this.let(
-                { count: composition.count },
+        retry({ count, components }) {
+            return composer.let(
+                { count },
                 params => ({ params }),
-                this.dowhile(
-                    this.finally(({ params }) => params, this.mask(this.retain_catch(...composition.components))),
-                    ({ result }) => result.error !== undefined && count-- > 0),
+                composer.dowhile(
+                    composer.finally(({ params }) => params, composer.mask(composer.retain_catch(...components))),
+                    composer.function('({ result }) => result.error !== undefined && count-- > 0')),
                 ({ result }) => result)
         },
+    }
 
-        combinators: {},
+    // recursively flatten composition into { composition, actions } by extracting embedded action definitions
+    function flatten(composition) {
+        if (arguments.length > 1) throw new ComposerError('Too many arguments')
+        if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+
+        const actions = []
+
+        const flatten = composition => {
+            composition = new Composition(composition) // copy
+            composition.visit(flatten)
+            if (composition.type === 'action' && composition.action) {
+                actions.push({ name: composition.name, action: composition.action })
+                delete composition.action
+            }
+            return composition
+        }
+
+        composition = flatten(composition)
+        return { composition, actions }
+    }
+
+    // synthesize composition code
+    function synthesize(composition) {
+        if (arguments.length > 1) throw new ComposerError('Too many arguments')
+        if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+        let code = `const main=(${main})().runtime(`
+        for (let plugin of plugins) {
+            code += `{plugin:new(${plugin.constructor})()`
+            if (plugin.configure) code += `,config:${JSON.stringify(plugin.configure())}`
+            code += '},'
+        }
+        code = require('uglify-es').minify(`${code})`, { output: { max_line_len: 127 } }).code
+        code = `// generated by composer v${version}\n\nconst composition = ${JSON.stringify(composition, null, 4)}\n\n// do not edit below this point\n\n${code}` // invoke conductor on composition
+        return { exec: { kind: 'nodejs:default', code }, annotations: [{ key: 'conductor', value: composition }, { key: 'composer', value: version }] }
+    }
+
+    composer.util = {
+        // return the signatures of the combinators
+        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(this.combinators, composition => this.deserialize(composition))
+            composition.visit(composition => composer.util.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(this.combinators, label(composition.path))
-                return composition
-            }
-
-            return label('')(composition)
-        },
-
-        // recursively label and lower combinators to the desired set of combinators (including primitive combinators)
+        // recursively 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))
+                combinators = Object.keys(composer.util.combinators).filter(key => semver.gte(combinators, composer.util.combinators[key].since))
             }
+            if (!Array.isArray(combinators)) throw new ComposerError('Invalid argument', combinators)
 
             const lower = composition => {
                 composition = new Composition(composition) // copy
                 // repeatedly lower root combinator
-                while (combinators.indexOf(composition.type) < 0 && this[`_${composition.type}`]) {
+                while (combinators.indexOf(composition.type) < 0 && lowerer[composition.type]) {
                     const path = composition.path
-                    composition = this[`_${composition.type}`](composition)
-                    if (path !== undefined) composition.path = path
+                    composition = lowerer[composition.type](composition)
+                    if (path !== undefined) composition.path = path // preserve path
                 }
                 // lower nested combinators
-                composition.visit(this.combinators, lower)
+                composition.visit(lower)
                 return composition
             }
 
@@ -288,43 +257,116 @@
         // register plugin
         register(plugin) {
             if (plugin.combinators) init(plugin.combinators())
-            if (plugin.composer) Object.assign(this, plugin.composer({ ComposerError, Composition }))
+            if (plugin.composer) Object.assign(composer, plugin.composer({ composer, ComposerError, Composition }))
+            if (plugin.lowerer) Object.assign(lowerer, plugin.lowerer({ composer, ComposerError, Composition }))
             plugins.push(plugin)
-            return this
+            return composer
+        },
+
+        /**
+         * 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
+         */
+        canonical(name) {
+            if (typeof name !== 'string') throw new ComposerError('Name must be a string')
+            if (name.trim().length == 0) throw new ComposerError('Name is not valid')
+            name = name.trim()
+            const delimiter = '/'
+            const parts = name.split(delimiter)
+            const n = parts.length
+            const 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') })
+            const newName = parts.join(delimiter)
+            if (leadingSlash) return newName
+            else if (n < 3) return `${delimiter}_${delimiter}${newName}`
+            else return `${delimiter}${newName}`
+        },
+
+        // encode composition as an action table
+        encode(name, composition, combinators) {
+            if (arguments.length > 3) throw new ComposerError('Too many arguments')
+            name = composer.util.canonical(name) // throws ComposerError if name is not valid
+            if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+            if (combinators) composition = composer.util.lower(composition, combinators)
+            const table = flatten(composition)
+            table.actions.push({ name, action: synthesize(table.composition) })
+            return table.actions
+        },
+
+        // return composer version
+        get version() {
+            return version
+        },
+
+        // 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)
+            return wsk
         },
     }
 
-    /**
-     * 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') throw new ComposerError('Name must be a string')
-        if (name.trim().length == 0) throw new ComposerError('Name is not valid')
-        name = name.trim()
-        const delimiter = '/'
-        const parts = name.split(delimiter)
-        const n = parts.length
-        const 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') })
-        const newName = parts.join(delimiter)
-        if (leadingSlash) return newName
-        else if (n < 3) return `${delimiter}_${delimiter}${newName}`
-        else return `${delimiter}${newName}`
+    // 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
+        }
+
+        // construct a composition object with the specified fields
+        constructor(composition) {
+            return Object.assign(this, composition)
+        }
+
+        // apply f to all fields of type composition
+        visit(f) {
+            const combinator = composer.util.combinators[this.type]
+            if (combinator.components) {
+                this.components = this.components.map(f)
+            }
+            for (let arg of combinator.args || []) {
+                if (arg.type === undefined && this[arg._] !== undefined) {
+                    this[arg._] = f(this[arg._], arg._)
+                }
+            }
+        }
     }
 
     // derive combinator methods from combinator table
     function init(combinators) {
-        Object.assign(composer.combinators, combinators)
+        Object.assign(composer.util.combinators, combinators)
         for (let type in combinators) {
             const combinator = combinators[type]
             // do not overwrite existing combinators
@@ -340,8 +382,7 @@
                     if (argument === undefined && arg.optional && arg.type !== undefined) continue
                     switch (arg.type) {
                         case undefined:
-                            composition[arg._] = this.task(arg.optional ? argument || null : argument)
-                            if (arg.named && composition[arg._].name === undefined) throw new ComposerError('Invalid argument', argument)
+                            composition[arg._] = composer.task(arg.optional ? argument || null : argument)
                             continue
                         case 'value':
                             if (typeof argument === 'function') throw new ComposerError('Invalid argument', argument)
@@ -355,144 +396,83 @@
                     }
                 }
                 if (combinator.components) {
-                    composition.components = Array.prototype.slice.call(arguments, skip).map(obj => {
-                        const task = composer.task(obj)
-                        if (combinator.components.named && task.name === undefined) throw new ComposerError('Invalid argument', obj)
-                        return task
-                    })
+                    composition.components = Array.prototype.slice.call(arguments, skip).map(obj => composer.task(obj))
                 }
                 return composition
             }
         }
     }
 
-    init(combinators)
+    init({
+        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' },
+        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' },
+        async: { args: [{ _: 'body' }], since: '0.6.0' },
+    })
 
-    // client-side stuff
-    function client() {
-        const os = require('os')
-        const path = require('path')
-        const minify = require('uglify-es').minify
-
-        // read composer version number
-        const version = require('./package.json').version
-
-        // 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.name === undefined) throw new ComposerError('Cannot deploy anonymous entity')
-                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)
-            }
+    // management class for compositions
+    class Compositions {
+        constructor(wsk) {
+            this.actions = wsk.actions
         }
 
-        // client-side only methods
-        Object.assign(composer, {
-            // 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(this.combinators, encode)
-                    if (composition.type === 'composition') {
-                        let code = `const main=(${main})().server(`
-                        for (let plugin of plugins) {
-                            code += `{plugin:new(${plugin.constructor})()`
-                            if (plugin.configure) code += `,config:${JSON.stringify(plugin.configure())}`
-                            code += '},'
-                        }
-                        code = minify(`${code})`, { output: { max_line_len: 127 } }).code
-                        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${code}` // 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 }
-            },
-
-            // return composer version
-            get version() {
-                return version
-            }
-        })
-
-        return composer
+        deploy(name, composition, combinators) {
+            const actions = composer.util.encode(name, composition, combinators)
+            return actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { }))
+                .then(() => this.actions.update(action)), Promise.resolve())
+                .then(() => actions)
+        }
     }
 
-    // server-side stuff
-    function server() {
-        function chain(front, back) {
-            front.slice(-1)[0].next = 1
-            front.push(...back)
-            return front
+    // runtime stuff
+    function runtime() {
+        // recursively label combinators with the json path
+        function 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)
         }
 
+        // compile ast to fsm
         const compiler = {
-            compile(node) {
-                if (arguments.length === 0) return [{ type: 'empty' }]
-                if (arguments.length === 1) return this[node.type](node)
-                return Array.prototype.map.call(arguments, node => this.compile(node)).reduce(chain)
-            },
-
             sequence(node) {
-                return chain([{ type: 'pass', path: node.path }], this.compile(...node.components))
+                return [{ type: 'pass', path: node.path }, ...compile(...node.components)]
             },
 
             action(node) {
-                return [{ type: 'action', name: node.name, async: node.async, path: node.path }]
+                return [{ type: 'action', name: node.name, path: node.path }]
+            },
+
+            async(node) {
+                const body = compile(node.body)
+                return [{ type: 'async', path: node.path, return: body.length + 2 }, ...body, { type: 'stop' }, { type: 'pass' }]
             },
 
             function(node) {
@@ -500,64 +480,56 @@
             },
 
             finally(node) {
-                var body = this.compile(node.body)
-                const finalizer = this.compile(node.finalizer)
-                var fsm = [[{ type: 'try', path: node.path }], body, [{ type: 'exit' }], finalizer].reduce(chain)
+                const finalizer = compile(node.finalizer)
+                const fsm = [{ type: 'try', path: node.path }, ...compile(node.body), { type: 'exit' }, ...finalizer]
                 fsm[0].catch = fsm.length - finalizer.length
                 return fsm
             },
 
             let(node) {
-                var body = this.compile(...node.components)
-                return [[{ type: 'let', let: node.declarations, path: node.path }], body, [{ type: 'exit' }]].reduce(chain)
+                return [{ type: 'let', let: node.declarations, path: node.path }, ...compile(...node.components), { type: 'exit' }]
             },
 
             mask(node) {
-                var body = this.compile(...node.components)
-                return [[{ type: 'let', let: null, path: node.path }], body, [{ type: 'exit' }]].reduce(chain)
+                return [{ type: 'let', let: null, path: node.path }, ...compile(...node.components), { type: 'exit' }]
             },
 
             try(node) {
-                var body = this.compile(node.body)
-                const handler = chain(this.compile(node.handler), [{ type: 'pass' }])
-                var fsm = [[{ type: 'try', path: node.path }], body, [{ type: 'exit' }]].reduce(chain)
-                fsm[0].catch = fsm.length
-                fsm.slice(-1)[0].next = handler.length
-                fsm.push(...handler)
+                const handler = [...compile(node.handler), { type: 'pass' }]
+                const fsm = [{ type: 'try', path: node.path }, ...compile(node.body), { type: 'exit' }, ...handler]
+                fsm[0].catch = fsm.length - handler.length
+                fsm[fsm.length - handler.length - 1].next = handler.length
                 return fsm
             },
 
             if_nosave(node) {
-                var consequent = this.compile(node.consequent)
-                var alternate = chain(this.compile(node.alternate), [{ type: 'pass' }])
-                var fsm = [[{ type: 'pass', path: node.path }], this.compile(node.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain)
-                consequent.slice(-1)[0].next = alternate.length
-                fsm.push(...consequent)
-                fsm.push(...alternate)
+                const consequent = compile(node.consequent)
+                const alternate = [...compile(node.alternate), { type: 'pass' }]
+                const fsm = [{ type: 'pass', path: node.path }, ...compile(node.test), { type: 'choice', then: 1, else: consequent.length + 1 }, ...consequent, ...alternate]
+                fsm[fsm.length - alternate.length - 1].next = alternate.length
                 return fsm
             },
 
             while_nosave(node) {
-                var consequent = this.compile(node.body)
-                var alternate = [{ type: 'pass' }]
-                var fsm = [[{ type: 'pass', path: node.path }], this.compile(node.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)
+                const body = compile(node.body)
+                const fsm = [{ type: 'pass', path: node.path }, ...compile(node.test), { type: 'choice', then: 1, else: body.length + 1 }, ...body, { type: 'pass' }]
+                fsm[fsm.length - 2].next = 2 - fsm.length
                 return fsm
             },
 
             dowhile_nosave(node) {
-                var test = this.compile(node.test)
-                var fsm = [[{ type: 'pass', path: node.path }], this.compile(node.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)
+                const fsm = [{ type: 'pass', path: node.path }, ...compile(node.body), ...compile(node.test), { type: 'choice', else: 1 }, { type: 'pass' }]
+                fsm[fsm.length - 2].then = 2 - fsm.length
                 return fsm
             },
         }
 
+        function compile(node) {
+            if (arguments.length === 0) return [{ type: 'empty' }]
+            if (arguments.length === 1) return compiler[node.type](node)
+            return Array.prototype.reduce.call(arguments, (fsm, node) => { fsm.push(...compile(node)); return fsm }, [])
+        }
+
         const openwhisk = require('openwhisk')
         let wsk
 
@@ -580,19 +552,6 @@
             },
 
             action({ p, node, index }) {
-                if (node.async) {
-                    if (!wsk) wsk = openwhisk({ ignore_certs: true })
-                    return wsk.actions.invoke({ name: node.name, params: p.params })
-                        .catch(error => {
-                            console.error(error)
-                            return { error: `An exception was caught at state ${index} (see log for details)` }
-                        })
-                        .then(result => {
-                            p.params = result
-                            inspect(p)
-                            return step(p)
-                        })
-                }
                 return { action: node.name, params: p.params, state: { $resume: p.s } }
             },
 
@@ -617,24 +576,43 @@
 
             pass({ p, node, index }) {
             },
+
+            async({ p, node, index, inspect, step }) {
+                if (!wsk) wsk = openwhisk({ ignore_certs: true })
+                p.params.$resume = { state: p.s.state }
+                p.s.state = index + node.return
+                return wsk.actions.invoke({ name: process.env.__OW_ACTION_NAME, params: p.params })
+                    .catch(error => {
+                        console.error(error)
+                        return { error: `An exception was caught at state ${index} (see log for details)` }
+                    })
+                    .then(result => {
+                        p.params = result
+                        inspect(p)
+                        return step(p)
+                    })
+            },
+
+            stop({ p, node, index, inspect, step }) {
+                p.s.state = -1
+            },
         }
 
         const finishers = []
 
-        for ({ plugin, config } of arguments) {
-            composer.register(plugin)
-            if (plugin.compiler) Object.assign(compiler, plugin.compiler())
+        for (let { plugin, config } of arguments) {
+            composer.util.register(plugin)
+            if (plugin.compiler) Object.assign(compiler, plugin.compiler({ compile }))
             if (plugin.conductor) {
-                const r = plugin.conductor(config)
-                if (r._finish) {
-                    finishers.push(r._finish)
-                    delete r._finish
+                Object.assign(conductor, plugin.conductor(config))
+                if (conductor._finish) {
+                    finishers.push(conductor._finish)
+                    delete conductor._finish
                 }
-                Object.assign(conductor, r)
             }
         }
 
-        const fsm = compiler.compile(composer.lower(composer.label(composer.deserialize(composition))))
+        const fsm = compile(composer.util.lower(label(composer.util.deserialize(composition))))
 
         // encode error object
         const encodeError = error => ({
@@ -651,17 +629,15 @@
             if (!isObject(p.params)) p.params = { value: p.params }
             if (p.params.error !== undefined) {
                 p.params = { error: p.params.error } // discard all fields but the error field
-                p.s.state = undefined // abort unless there is a handler in the stack
+                p.s.state = -1 // abort unless there is a handler in the stack
                 while (p.s.stack.length > 0) {
-                    if (typeof (p.s.state = p.s.stack.shift().catch) === 'number') break
+                    if ((p.s.state = p.s.stack.shift().catch || -1) >= 0) break
                 }
             }
         }
 
         // run function f on current stack
         function run(f, p) {
-            this.require = require
-
             // handle let/mask pairs
             const view = []
             let n = 0
@@ -699,7 +675,7 @@
 
         function step(p) {
             // final state, return composition result
-            if (p.s.state === undefined) {
+            if (p.s.state < 0 || p.s.state >= fsm.length) {
                 console.log(`Entering final state`)
                 console.log(JSON.stringify(p.params))
                 return finishers.reduce((promise, _finish) => promise.then(() => _finish(p)), Promise.resolve())
@@ -709,8 +685,8 @@
             // process one state
             const node = fsm[p.s.state] // json definition for index state
             if (node.path !== undefined) console.log(`Entering composition${node.path}`)
-            const index = p.s.state // save current state for logging purposes
-            p.s.state = node.next === undefined ? undefined : p.s.state + node.next // default next state
+            const index = p.s.state // current state
+            p.s.state = p.s.state + (node.next || 1) // default next state
             return conductor[node.type]({ p, index, node, inspect, step }) || step(p)
         }
 
@@ -724,9 +700,8 @@
                 if (!isObject(params.$resume)) return badRequest('The type of optional $resume parameter must be object')
                 const resuming = params.$resume.stack
                 Object.assign(p.s, params.$resume)
-                if (resuming) p.s.state = params.$resume.state // undef
-                if (p.s.state !== undefined && typeof p.s.state !== 'number') return badRequest('The type of optional $resume.state parameter must be number')
-                if (!Array.isArray(p.s.stack)) return badRequest('The type of $resume.stack must be an array')
+                if (typeof p.s.state !== 'number') return badRequest('The type of optional $resume.state parameter must be number')
+                if (!Array.isArray(p.s.stack)) return badRequest('The type of optional $resume.stack parameter must be an array')
                 delete params.$resume
                 if (resuming) inspect(p) // handle error objects when resuming
             }
@@ -735,7 +710,7 @@
         }
     }
 
-    return { client, server }
+    return { composer, runtime }
 }
 
-module.exports = main().client()
+module.exports = main().composer
diff --git a/docs/COMBINATORS.md b/docs/COMBINATORS.md
index 6989ac2..83af8c9 100644
--- a/docs/COMBINATORS.md
+++ b/docs/COMBINATORS.md
@@ -7,9 +7,9 @@
 | [`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')` |
+| [`task`](#task) | single task | `composer.task('echo')`
 | [`let`](#let) | variable declarations | `composer.let({ count: 3, message: 'hello' }, ...)` |
 | [`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')` |
@@ -20,6 +20,7 @@
 | [`finally`](#finally) | finalization | `composer.finally('tryThis', 'doThatAlways')` |
 | [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` |
 | [`retain` and `retain_catch`](#retain) | persistence | `composer.retain('validateInput')` |
+| [`async`](#async) | asynchronous invocation | `composer.async('sendMessage')` |
 
 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.
 
@@ -32,7 +33,7 @@
 
 ## 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`.
+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.
 
 ## Action
 
@@ -136,19 +137,6 @@
 
 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.
@@ -163,6 +151,10 @@
 
 An empty sequence behaves as a sequence with a single function `params => params`. The output parameter object for the empty sequence is its input parameter object unless it is an error object, in which case, as usual, the error object only contains the `error` field of the input parameter object.
 
+## Task
+
+`composer.task(composition)` is equivalent to `composer.sequence(composition)`.
+
 ## Let
 
 `composer.let({ name_1: value_1, name_2: value_2, ... }, composition_1_, _composition_2_, ...)` declares one or more variables with the given names and initial values, and runs a sequence of compositions in the scope of these declarations.
@@ -266,3 +258,7 @@
 `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_.
 
 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.
+
+## Async
+
+`composer.async(body)` runs the _body_ composition asynchronously. It spawns _body_ but does not wait for it to execute. It immediately returns a dictionary with a single field named `activationId` identifying the invocation of _body_.
diff --git a/docs/COMPOSE.md b/docs/COMPOSE.md
index 4750fa2..a1c1f03 100644
--- a/docs/COMPOSE.md
+++ b/docs/COMPOSE.md
@@ -24,6 +24,8 @@
   -u, --auth KEY         authorization KEY
   -i, --insecure         bypass certificate checking
   -v, --version          output the composer version
+  --quiet                omit detailed diagnostic messages
+  --composer COMPOSER    instantiate a custom composer module
 ```
 The `compose` command requires either a Javascript file that evaluates to a composition (for example [demo.js](../samples/demo.js)) or a JSON file that encodes a composition (for example [demo.json](../samples/demo.json)). The JSON format is documented in [FORMAT.md](FORMAT.md).
 
@@ -74,12 +76,6 @@
         }
     }
 }
-```
-The evaluation context includes the `composer` object implicitly defined as:
-```javascript
-composer = require('@ibm-functions/composer')
-```
-In other words, there is no need to require the `composer` module explicitly in the composition code.
 
 ## Entity option
 
@@ -253,8 +249,12 @@
 
 ## 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 VERSION` option is specified, the `compose` command uses the set of combinators of the specified revision of the `composer` module. Derived combinators that are more recent (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.
+
+## Composer option
+
+If the composition code uses a custom `composer` module, the path to the module must be specified via the `--composer` option.
\ No newline at end of file
diff --git a/docs/COMPOSER.md b/docs/COMPOSER.md
index 71efd2e..cf76add 100644
--- a/docs/COMPOSER.md
+++ b/docs/COMPOSER.md
@@ -14,6 +14,7 @@
 
 The [samples/node-demo.js](../samples/node-demo.js) file illustrates how to define, deploy, and invoke a composition using `node`: 
 ```javascript
+
 // require the composer module
 const composer = require('@ibm-functions/composer')
 
@@ -24,13 +25,12 @@
     composer.action('failure', { action: function () { return { message: 'failure' } } }))
 
 // instantiate OpenWhisk client
-const wsk = composer.openwhisk({ ignore_certs: true })
+const wsk = composer.util.openwhisk({ ignore_certs: true })
 
-wsk.compositions.deploy(composer.composition('demo', composition)) // name and deploy composition
+wsk.compositions.deploy('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)
 ```
-```
 node samples/node-demo.js
 ```
 ```json
@@ -40,67 +40,69 @@
 ```
 Alternatively, the `compose` command can deploy compositions and the OpenWhisk CLI can invoke compositions. See [COMPOSE.md](COMPOSE.md) for details.
 
-# Composer methods
+# Helper 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). It also offers a series of helper methods described below:
+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 via the `composer.util` object.
 
-| Combinator | Description | Example |
+| Helper method  | 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')` |
+| [`version`](#version) | `composer.util.version` |
+| [`deserialize`](#deserialize) | `composer.util.deserialize(JSON.stringify(composition))` |
+| [`canonical`](#canonical) | `composer.util.canonical('demo')` |
+| [`lower`](#lower) | `composer.util.lower(composer.if('authenticate', 'success', 'failure'), '0.4.0')` |
+| [`encode`](#encode) | `composer.util.encode('demo', composition, '0.4.0')` |
+| [`openwhisk`](#openwhisk-client) | `composer.util.openwhisk()` |
 
-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.
+## Version
+
+`composer.util.version` returns the version number for the composer module.
 
 ## Deserialize
 
-`composer.deserialize(composition)` recursively deserializes a serialized composition object. In other words, it recreates a `Composition` object from the input JSON dictionary.
+`composer.util.deserialize(composition)` recursively deserializes a serialized composition object. In other words, it recreates a `Composition` object from the input JSON dictionary.
+
+## Canonical
+
+`composer.util.canonical(name)` attempts to validate and expand the action name `name` to its canonical form.
 
 ## 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.
+`composer.util.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.
 
-For instance, `composer.lower(composition, ['retry'])` will preserve any instance of the `retry` combinator but replace other non-primitive combinators sur as `retain`.
+For instance, `composer.util.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.
+`composer.util.encode(name, composition, [combinators])` first invokes `composer.util.lower` on the composition with the specified `combinators` argument if any. It then encodes the composition as an array of actions. This array consists of all the actions defined as part of the composition plus the conductor action synthesized for the composition itself.
 
-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.
+The optional `combinators` parameter controls the optional lowering. See [lower](#lower) for details.
 
 ## Openwhisk client
 
-A client instance is obtained by invoking `composer.openwhisk([options])`, for instance with:
+The `composer` object offers an extension to the [OpenWhisk Client for Javascript](https://github.com/apache/incubator-openwhisk-client-js) that supports deploying compositions.
+
+An OpenWhisk client instance is obtained by invoking `composer.util.openwhisk([options])`, for instance with:
 ```javascript
-const wsk = composer.openwhisk({ ignore_certs: true })
+const wsk = composer.util.openwhisk({ ignore_certs: true })
 
 ```
 The specific OpenWhisk deployment to use may be specified via the optional `options` argument, environment variables, or the OpenWhisk property file. Options have priority over environment variables, which have priority over the OpenWhisk property file. Options and environment variables are documented [here](https://github.com/apache/incubator-openwhisk-client-js#constructor-options). The default path for the whisk property file is `$HOME/.wskprops`. It can be altered by setting the `WSK_CONFIG_FILE` environment variable.
 
 The `composer` module adds to the OpenWhisk client instance a new top-level category named `compositions` with a method named `deploy`.
 
-## Deploying compositions
+### Deploying compositions
 
-`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))
-```
+`wsk.compositions.deploy(name, composition, [combinators])` optionally lowers, encodes, and deploys the composition `composition`. More precisely, it successively deploys all the actions defined in `composition` as well as `composition` itself (encoded as a conductor action).
 
-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.
+The optional `combinators` parameter controls the optional lowering. See [lower](#lower) for details.
 
 The `deploy` method returns a successful promise if all the actions were deployed successfully, or a rejected promise otherwise. In the later, the state of the various actions is unknown.
 
 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.
 
-## Invoking, updating, and deleting compositions
+### 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:
+Since compositions are deployed as conductor actions, other management tasks for compositions can be achieved by invoking methods of `wsk.actions`. For example, to delete a composition named `demo`, use command:
 ```javascript
 wsk.actions.delete('demo')
 ```
diff --git a/docs/COMPOSITIONS.md b/docs/COMPOSITIONS.md
index 4c8e416..87f51a0 100644
--- a/docs/COMPOSITIONS.md
+++ b/docs/COMPOSITIONS.md
@@ -37,18 +37,17 @@
 
 Javascript functions can be viewed as simple, anonymous actions that do not need to be deployed and managed separately from the composition they belong to. Functions are typically used to alter a parameter object between two actions that expect different schemas, as in:
 ```javascript
-composer.if('getUserNameAndPassword', params => ({ key = btoa(params.user + ':' + params.password) }), 'authenticate')
+composer.sequence('getUserNameAndPassword', params => ({ key = btoa(params.user + ':' + params.password) }), 'authenticate')
 ```
-
-Compositions may be nested inside compositions in two ways. First, combinators can be nested, e.g.,
+Combinators can be nested, e.g.,
 ```javascript
 composer.if('isEven', 'half', composer.sequence('triple', 'increment'))
 ```
-Second, compositions can reference other compositions by name. For instance, assuming we deploy the sequential composition of the `triple` and `increment` actions as the composition `tripleAndIncrement`, the following code behaves identically to the previous example:
+Compositions can reference other compositions by name. For instance, assuming we deploy the sequential composition of the `triple` and `increment` actions as the composition `tripleAndIncrement`, the following code behaves identically to the previous example:
 ```javascript
 composer.if('isEven', 'half', 'tripleAndIncrement')
 ```
-Observe however, that the behavior of the second composition would be altered if we redefine the `tripleAndIncrement` composition to do something else, whereas the first example would not be affected.
+The behavior of this last composition would be altered if we redefine the `tripleAndIncrement` composition to do something else, whereas the first example would not be affected.
 
 ## Nested declarations
 
@@ -62,18 +61,10 @@
 ```
 Deploying such a composition deploys the embedded actions.
 
-A composition can also include the definition of another 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 other words, 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.
-
 ## Serialization and deserialization
 
  Compositions objects can be serialized to JSON dictionaries by invoking `JSON.stringify` on them. Serialized compositions can be deserialized to composition objects using the `composer.deserialize(serializedComposition)` method. The JSON format is documented in [FORMAT.md](FORMAT.md).
- In short, the JSON dictionary for a composition contains a representation of the syntax tree for this composition as well as the definition of all the actions and compositions embedded inside the composition.
+ In short, the JSON dictionary for a composition contains a representation of the syntax tree for this composition as well as the definition of all the actions embedded inside the composition.
 
 ## Conductor actions
 
diff --git a/docs/FORMAT.md b/docs/FORMAT.md
index 6822db1..47f0901 100644
--- a/docs/FORMAT.md
+++ b/docs/FORMAT.md
@@ -43,9 +43,8 @@
 | Type | Fields |
 | --:| --- | 
 | `action` | name:string, action:optional object |
-| `function` | function:string |
-| `literal` or `value` | value:any |
-| `composition` | name:string, composition:composition |
+| `function` | function:object |
+| `literal` or `value` | value:json value |
 | `empty` |
 | `sequence` or `seq` | components:array of compositions |
 | `let` | declarations:object, components:array of compositions |
@@ -58,3 +57,4 @@
 | `finally` | body:composition, finalizer:composition |
 | `retry` | count:number, components:array of compositions |
 | `retain` and `retain_catch` | components:array of compositions |
+| `async` | body:composition |
\ No newline at end of file
diff --git a/samples/demo.js b/samples/demo.js
index 71cc939..f5e3805 100644
--- a/samples/demo.js
+++ b/samples/demo.js
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-composer.if(
+const composer = require('@ibm-functions/composer')
+
+module.exports = composer.if(
     composer.action('authenticate', { action: function ({ password }) { return { value: password === 'abc123' } } }),
     composer.action('success', { action: function () { return { message: 'success' } } }),
     composer.action('failure', { action: function () { return { message: 'failure' } } }))
diff --git a/samples/node-demo.js b/samples/node-demo.js
index 5956715..62a986d 100644
--- a/samples/node-demo.js
+++ b/samples/node-demo.js
@@ -24,8 +24,8 @@
     composer.action('failure', { action: function () { return { message: 'failure' } } }))
 
 // instantiate OpenWhisk client
-const wsk = composer.openwhisk({ ignore_certs: true })
+const wsk = composer.util.openwhisk({ ignore_certs: true })
 
-wsk.compositions.deploy(composer.composition('demo', composition)) // deploy composition
+wsk.compositions.deploy('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 ece8237..d0ba083 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1,14 +1,13 @@
 const assert = require('assert')
 const composer = require('../composer')
 const name = 'TestAction'
-const compositionName = 'TestComposition'
-const wsk = composer.openwhisk({ ignore_certs: process.env.IGNORE_CERTS && process.env.IGNORE_CERTS !== 'false' && process.env.IGNORE_CERTS !== '0' })
+const wsk = composer.util.openwhisk({ ignore_certs: process.env.IGNORE_CERTS && process.env.IGNORE_CERTS !== 'false' && process.env.IGNORE_CERTS !== '0' })
 
 // deploy action
 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(composer.composition(name, task)).then(() => wsk.actions.invoke({ name, params, blocking }))
+const invoke = (composition, params = {}, blocking = true) => wsk.compositions.deploy(name, composition).then(() => wsk.actions.invoke({ name, params, blocking }))
 
 describe('composer', function () {
     this.timeout(60000)
@@ -21,7 +20,6 @@
             .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 () {
@@ -33,7 +31,7 @@
             })
 
             it('action must return activationId', function () {
-                return invoke(composer.action('isNotOne', { async: true }), { n: 1 }).then(activation => assert.ok(activation.response.result.activationId))
+                return invoke(composer.async('isNotOne'), { n: 1 }).then(activation => assert.ok(activation.response.result.activationId))
             })
 
             it('action name must parse to fully qualified', function () {
@@ -89,43 +87,6 @@
             })
         })
 
-        describe('compositions', function () {
-            it('composition must return true', function () {
-                return invoke(composer.composition(compositionName, composer.action('isNotOne')), { n: 0 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
-            })
-
-            it('action must return activationId', function () {
-                return invoke(composer.composition(compositionName, composer.action('isNotOne'), { async: true }), { n: 1 }).then(activation => assert.ok(activation.response.result.activationId))
-            })
-
-            it('invalid argument', function () {
-                try {
-                    invoke(composer.composition(compositionName, 42))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid argument'))
-                }
-            })
-
-            it('invalid options', function () {
-                try {
-                    invoke(composer.composition(compositionName, 'foo', 42))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid argument'))
-                }
-            })
-
-            it('too many arguments', function () {
-                try {
-                    invoke(composer.composition(compositionName, 'foo', {}, 'foo'))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Too many arguments'))
-                }
-            })
-        })
-
         describe('literals', function () {
             it('true', function () {
                 return invoke(composer.literal(true)).then(activation => assert.deepEqual(activation.response.result, { value: true }))
@@ -214,7 +175,7 @@
                         "name": "echo"
                     }]
                 }
-                return invoke(composer.deserialize(json), { message: 'hi' }).then(activation => assert.deepEqual(activation.response.result, { message: 'hi' }))
+                return invoke(composer.util.deserialize(json), { message: 'hi' }).then(activation => assert.deepEqual(activation.response.result, { message: 'hi' }))
             })
         })