Prepare composer for Apache handoff (#66)

* Prep work for new release

* Update setup.sh

* Update .gitignore

* Ignore certs in async invoke for now

* Replace uglify-es with terser

* Bump standard version to 12.0.1

* Delete outdated content. Update documentation.
diff --git a/.gitignore b/.gitignore
index 3c3629e..ef84937 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 node_modules
+openwhisk
diff --git a/.travis.yml b/.travis.yml
index c13ce0b..2d444b3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,5 @@
 env:
   global:
     - IGNORE_CERTS=true
-    - REDIS=redis://172.17.0.1:6379
 before_script:
   - ./travis/setup.sh
diff --git a/CLA-CORPORATE.md b/CLA-CORPORATE.md
deleted file mode 100644
index 952896b..0000000
--- a/CLA-CORPORATE.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# International Business Machines, Inc. (IBM)
-### Software Grant and Corporate Contributor License Agreement ("Agreement")
-
-https://github.com/ibm-functions/composer/
-
-Thank you for your interest in IBM’s Composer project ("the Project").
-
-In order to clarify the intellectual property license granted with Contributions from any person or entity, IBM must have a Contributor License Agreement (CLA) on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of IBM and its users; it does not change your rights to use your own Contributions for any other purpose.
-
-This version of the Agreement allows an entity (the "Corporation") to submit Contributions to the Project, to authorize Contributions submitted by its designated employees to the Project, and to grant copyright and patent licenses thereto.
-
-If you have not already done so, please complete and sign, then scan and email a pdf file of this Agreement to tardieu@us.ibm.com. If necessary, send an original signed agreement to:
-
-    IBM Corporation, 1101 Kitchawan Rd Route 134 / PO Box 218 Yorktown Heights, NY 10598 Attn: Olivier Tardieu
-
-Please read this document carefully before signing and keep a copy for your records.
-
-    Corporation name:
-    Corporation address:
-    Point of Contact:
-    E-Mail:
-    Telephone:
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the Project. Except for the license granted herein to IBM and recipients of software distributed by IBM, You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.    
-  "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with IBM. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.    
-  "Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to IBM for inclusion in, or documentation of, the Project managed by IBM (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to IBM or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, IBM for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to IBM and to recipients of software distributed by IBM a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to IBM and to recipients of software distributed by IBM a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) were submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation designated on Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation.
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to IBM separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
-
-8. It is your responsibility to notify IBM when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with IBM.
-
-Please sign and date: ______________________________________________________________
-
-    Title:
-    Corporation:
-
-Schedule A
-
-[Initial list of designated employees. NB: authorization is not tied to particular Contributions.]
-
-Schedule B
-
-[Identification of optional concurrent software grant. Would be left blank or omitted if there is no concurrent software grant.]
-
diff --git a/CLA-INDIVIDUAL.md b/CLA-INDIVIDUAL.md
deleted file mode 100644
index 7846733..0000000
--- a/CLA-INDIVIDUAL.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# International Business Machines, Inc. (IBM)
-### Individual Contributor License Agreement ("Agreement")
-
-https://github.com/ibm-functions/composer/
-
-Thank you for your interest in the Composer project ("the Project").
-
-In order to clarify the intellectual property license granted with Contributions from any person or entity, IBM must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of IBM and its customers; it does not change your rights to use your own Contributions for any other purpose.
-
-If you have not already done so, please complete and sign, then scan and email a pdf file of this Agreement to tardieu@us.ibm.com. If necessary, send an original signed agreement to:
-
-    IBM Corporation, 1101 Kitchawan Rd Route 134 / PO Box 218 Yorktown Heights, NY 10598 Attn: Olivier Tardieu
-
-Please read this document carefully before signing and keep a copy for your records.
-
-    Full name:
-    GitHub Username:
-    Address:
-    Country:
-    E-Mail:
-    Telephone:
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the Project. Except for the license granted herein to IBM and recipients of software distributed by IBM, You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.    
-  "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with IBM. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.  
-  "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the Project for inclusion in, or documentation of, the Project (”the Work”). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Project or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to IBM and to recipients of software distributed by IBM a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to IBM and to recipients of software distributed by IBM a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work to which Your Contribution(s) were submitted, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to the Project, or that your employer has executed a separate Corporate CLA with IBM.
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to the Project separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
-
-8. You agree to notify IBM of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
-
-Please sign and date: ______________________________________________________________
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index be6a15b..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Contributing to Composer
-
-We welcome contributions, but request you follow these guidelines.
-
- - [Raising issues](#raising-issues)
- - [Contributor License Agreement](#contributor-license-agreement)
- - [Coding Standards](#coding-standards)
- 
-## Raising issues
-
-Please raise any bug reports on the issue tracker. Be sure to
-search the list to see if your issue has already been raised.
-
-A good bug report is one that make it easy for us to understand what you were
-trying to do and what went wrong. Provide as much context as possible so we can try to recreate the issue.
-
-
-### Contributor License Agreement
-
-In order for us to accept pull-requests, the contributor must first complete
-a Contributor License Agreement (CLA). This clarifies the intellectual
-property license granted with any contribution. It is for your protection as a
-Contributor as well as the protection of IBM and its clients; it does not
-change your rights to use your own Contributions for any other purpose.
-
-You can download the CLAs here:
-
-- [individual](CLA-INDIVIDUAL.md)
-- [corporate](CLA-CORPORATE.md)
-
-
-### Coding standards
-
-Please ensure you follow the coding standards used throughout the existing
-code base. Some basic rules include:
-
- - all files must have the Apache license in the header.
- - all PRs must have passing builds for all operating systems.
diff --git a/README.md b/README.md
index a1e1637..a86afb2 100644
--- a/README.md
+++ b/README.md
@@ -5,56 +5,33 @@
 [![Join
 Slack](https://img.shields.io/badge/join-slack-9B69A0.svg)](http://slack.openwhisk.org/)
 
-Composer is a new programming model from [IBM
-Research](https://ibm.biz/serverless-research) for composing [IBM Cloud
-Functions](https://ibm.biz/openwhisk), built on [Apache
-OpenWhisk](https://github.com/apache/incubator-openwhisk). With Composer,
-developers can build even more serverless applications including using it for
-IoT, with workflow orchestration, conversation services, and devops automation,
-to name a few examples.
+Composer is a new programming model for composing cloud functions built on
+[Apache OpenWhisk](https://github.com/apache/incubator-openwhisk). With
+Composer, developers can build even more serverless applications including using
+it for IoT, with workflow orchestration, conversation services, and devops
+automation, to name a few examples.
 
-Programming compositions for IBM Cloud Functions is supported by a new developer
-tool called [IBM Cloud Shell](https://github.com/ibm-functions/shell), or just
-_Shell_. Shell offers a CLI and graphical interface for fast, incremental,
-iterative, and local development of serverless applications. While we recommend
-using Shell, Shell is not required to work with compositions. Compositions may
-be managed using a combination of the Composer [compose](docs/COMPOSE.md) command
-(for deployment) and the [OpenWhisk
-CLI](https://console.bluemix.net/openwhisk/learn/cli) (for configuration,
-invocation, and life-cycle management).
-
-**In contrast to earlier releases of Composer, a Redis server is not required to
-run compositions**. Composer now synthesizes OpenWhisk [conductor
+Composer synthesizes OpenWhisk [conductor
 actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md)
 to implement compositions. Compositions have all the attributes and capabilities
-of an action (e.g., default parameters, limits, blocking invocation, web
-export).
+of an action, e.g., default parameters, limits, blocking invocation, web export.
 
 This repository includes:
-* the [composer](docs/COMPOSER.md) Node.js module for authoring compositions using
+* the [composer](composer.js) Node.js module for authoring compositions using
   JavaScript,
-* the [compose](docs/COMPOSE.md) command for deploying compositions,
+* the [compose](bin/compose.js) and [deploy](bin/deploy.js)
+  [commands](docs/COMMANDS.md) for compiling and deploying compositions,
 * [documentation](docs), [examples](samples), and [tests](test).
 
-Composer and Shell are currently available as _IBM Research previews_. As
-Composer and Shell continue to evolve, it may be necessary to redeploy existing
-compositions to take advantage of new capabilities. However existing
-compositions should continue to run fine without redeployment.
-
 ## Installation
 
 Composer is distributed as Node.js package. To install this package, use the
 Node Package Manager:
 ```
-npm install @ibm-functions/composer
+npm install -g @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
-```
-Shell embeds the Composer package, so there is no need to install
-Composer explicitly when using Shell.
+We recommend to install the package globally (with `-g` option) if you intend to
+use the `compose` and `deploy` commands to compile and deploy compositions.
 
 ## Defining a composition
 
@@ -68,13 +45,13 @@
     composer.action('success', { action: function () { return { message: 'success' } } }),
     composer.action('failure', { action: function () { return { message: 'failure' } } }))
 ```
-Compositions compose actions using _combinator_ methods. These methods
-implement the typical control-flow constructs of a sequential imperative
-programming language. This example composition composes three actions named
-`authenticate`, `success`, and `failure` using the `composer.if` combinator,
-which implements the usual conditional construct. It take three actions (or
-compositions) as parameters. It invokes the first one and, depending on the
-result of this invocation, invokes either the second or third action.
+Compositions compose actions using [combinator](docs/COMBINATORS.md) methods.
+These methods implement the typical control-flow constructs of a sequential
+imperative programming language. This example composition composes three actions
+named `authenticate`, `success`, and `failure` using the `composer.if`
+combinator, which implements the usual conditional construct. It take three
+actions (or compositions) as parameters. It invokes the first one and, depending
+on the result of this invocation, invokes either the second or third action.
 
  This composition includes the definitions of the three composed actions. If the
  actions are defined and deployed elsewhere, the composition code can be shorten
@@ -85,16 +62,19 @@
 
 ## Deploying a composition
 
-One way to deploy a composition is to use the [compose](docs/COMPOSE.md) command:
+One way to deploy a composition is to use the `compose` and `deploy` commands:
 ```
-compose demo.js --deploy demo
+compose demo.js > demo.json
+deploy demo demo.json -w
 ```
 ```
 ok: created /_/authenticate,/_/success,/_/failure,/_/demo
 ```
-The `compose` command synthesizes and deploys an action named `demo` that
-implements the composition. It also deploys the composed actions if definitions
-are provided for them.
+The `compose` command compiles the composition code to a portable JSON format.
+The `deploy` command deploys the JSON-encoded composition creating an action
+with the given name. It also deploys the composed actions if definitions are
+provided for them. The `-w` option authorizes the `deploy` command to overwrite
+existing definitions.
 
 ## Running a composition
 
@@ -116,7 +96,7 @@
     "message": "failure"
 }
 ```
-## Execution traces
+### Execution traces
 
 This invocation creates a trace, i.e., a series of activation records:
 ```
@@ -143,55 +123,3 @@
 [documentation of conductor
 actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md)
 explains execution traces in greater details.
-
-## Getting started 
-* [Introduction to Serverless
-  Composition](docs/tutorials/introduction/README.md): Setting up your
-  programming environment and getting started with Shell and Composer.
-* [Building a Translation Slack Bot with Serverless
-  Composition](docs/tutorials/translateBot/README.md): A more advanced tutorial
-  using Composition to build a serverless Slack chatbot that does language
-  translation.
-* [Composer Reference](docs/README.md): A comprehensive reference manual for the
-  Node.js programmer.
-
-## Videos
-* The [IBM Cloud Shell YouTube
-  channel](https://www.youtube.com/channel/UCcu16nIMNclSujJWDOgUI_g) hosts demo
-  videos of IBM Cloud Shell, including editing a composition [using a built-in
-  editor](https://youtu.be/1wmkSYl7EDM) or [an external
-  editor](https://youtu.be/psqoysnVgE4), and [visualizing a composition's
-  execution](https://youtu.be/jTaHgDQDZnQ).
-* Watch [our presentation at
-  Serverlessconf'17](https://acloud.guru/series/serverlessconf/view/ibm-cloud-functions)
-  about Composer and Shell.
-* [Conductor Actions and Composer
-  v2](https://urldefense.proofpoint.com/v2/url?u=https-3A__youtu.be_qkqenC5b1kE&d=DwIGaQ&c=jf_iaSHvJObTbx-siA1ZOg&r=C3zA0dhyHjF4WaOy8EW8kQHtYUl9-dKPdS8OrjFeQmE&m=vCx7thSf3YtT7x3Pe2DaLYw-dcjU1hNIfDkTM_21ObA&s=MGh9y3vSvssj1xTzwEurJ6TewdE7Dr2Ycs10Tix8sNg&e=)
-  (29:30 minutes into the video): A discussion of the composition runtime.
-
-## Blog posts
-* [Serverless Composition with IBM Cloud
-  Functions](https://www.raymondcamden.com/2017/10/09/serverless-composition-with-ibm-cloud-functions/)
-* [Building Your First Serverless Composition with IBM Cloud
-  Functions](https://www.raymondcamden.com/2017/10/18/building-your-first-serverless-composition-with-ibm-cloud-functions/)
-* [Upgrading Serverless Superman to IBM
-  Composer](https://www.raymondcamden.com/2017/10/20/upgrading-serverless-superman-to-ibm-composer/)
-* [Calling Multiple Serverless Actions and Retaining Values with IBM
-  Composer](https://www.raymondcamden.com/2017/10/25/calling-multiple-serverless-actions-and-retaining-values-with-ibm-composer/)
-* [Serverless Try/Catch/Finally with IBM
-  Composer](https://www.raymondcamden.com/2017/11/22/serverless-trycatchfinally-with-ibm-composer/)
-* [Composing functions into
-  applications](https://medium.com/openwhisk/composing-functions-into-applications-70d3200d0fac)
-* [A composition story: using IBM Cloud Functions to relay SMS to
-  email](https://medium.com/openwhisk/a-composition-story-using-ibm-cloud-functions-to-relay-sms-to-email-d67fc65d29c)
-* [Data Flows in Serverless Cloud-Native
-  Applications](http://heidloff.net/article/serverless-data-flows)
-* [Transforming JSON Data in Serverless
-  Applications](http://heidloff.net/article/transforming-json-serverless)
-
-## Contributions
-We are looking forward to your feedback and criticism. We encourage you to [join
-us on slack](http://ibm.biz/composer-users). File bugs and we will squash them.
-
-We welcome contributions to Composer and Shell. See
-[CONTRIBUTING.md](CONTRIBUTING.md).
diff --git a/bin/compose b/bin/compose
deleted file mode 100755
index 0110160..0000000
--- a/bin/compose
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env node
-
-'use strict'
-
-const minimist = require('minimist')
-const path = require('path')
-
-const argv = minimist(process.argv.slice(2), {
-    string: ['apihost', 'auth', 'deploy', 'lower', 'entity', 'entities', 'composer'],
-    boolean: ['insecure', 'encode', 'json', 'version', 'quiet'],
-    alias: { auth: 'u', insecure: 'i', version: 'v' }
-})
-let { util } = require(argv.composer || '../composer')
-
-if (argv.version) {
-    console.log(util.version)
-    return
-}
-
-let count = 0
-if (argv.json) count++
-if (argv.encode) count++
-if (argv.entity) count++
-if (argv.entities) count++
-if (typeof argv.deploy !== 'undefined') count++
-
-if (argv._.length !== 1 || count > 1 || argv.deploy === '' || argv.entity === '' || argv.entities === '') {
-    console.error('Usage:')
-    console.error('  compose composition.js[on] command [flags]')
-    console.error('Commands:')
-    console.error('  --json                 output the json representation for the composition (default command)')
-    console.error('  --deploy NAME          deploy the composition with name NAME')
-    console.error('  --entity NAME          output the conductor action definition for the composition (giving name NAME to the composition)')
-    console.error('  --entities NAME        convert the composition into an array of action definition (giving name NAME to the composition)')
-    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')
-    console.error('  --quiet                omit detailed diagnostic messages')
-    console.error('  --composer COMPOSER    instantiate a custom composer module')
-    process.exit(127)
-}
-
-try {
-    const filename = argv._[0]
-    let composition
-    try {
-        composition = require(path.resolve(filename))
-        if (composition.constructor && composition.constructor.name === 'Composition') {
-            util = composition.constructor.composer.util
-        } else {
-            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)
-        process.exit(4)
-    }
-    if (argv.deploy) {
-        const options = { ignore_certs: argv.insecure }
-        if (argv.apihost) options.apihost = argv.apihost
-        if (argv.auth) options.api_key = argv.auth
-        return Promise.resolve()
-            .then(() => util.frontend(options).compositions.deploy({ name: argv.deploy, composition }))
-            .then(actions => {
-                const names = actions.map(action => action.name)
-                console.log(`ok: created ${names}`)
-            })
-            .catch(error => {
-                switch (error.statusCode) {
-                    case 401:
-                        console.error('Authentication failure')
-                        if (!argv.quiet) console.log(error)
-                        process.exit(2)
-                    case 403:
-                        console.error('Authorization failure')
-                        if (!argv.quiet) console.log(error)
-                        process.exit(3)
-                }
-                console.error('Server error')
-                if (!argv.quiet) console.log(error)
-                process.exit(5)
-            })
-    } else if (argv.encode) {
-        console.log(util.encode('noname', composition).slice(-1)[0].action.exec.code)
-    } else if (argv.entity) {
-        console.log(JSON.stringify(util.encode(argv.entity, composition).slice(-1)[0], null, 4))
-    } else if (argv.entities) {
-        console.log(JSON.stringify(util.encode(argv.entities, composition), null, 4))
-    } else {
-        console.log(JSON.stringify(composition, null, 4))
-    }
-} catch (error) {
-    console.error('Internal error')
-    if (!argv.quiet) console.log(error)
-    process.exit(1)
-}
diff --git a/bin/compose.js b/bin/compose.js
new file mode 100755
index 0000000..8821eba
--- /dev/null
+++ b/bin/compose.js
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+
+/*
+ * Copyright 2017-2018 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict'
+
+const composer = require('../composer')
+const json = require('../package.json')
+const minimist = require('minimist')
+const Module = require('module')
+const path = require('path')
+
+const argv = minimist(process.argv.slice(2), {
+  boolean: ['version', 'ast'],
+  alias: { version: 'v' }
+})
+
+if (argv.version) {
+  console.log(json.version)
+  process.exit(0)
+}
+
+// resolve module even if not in default path
+const _resolveFilename = Module._resolveFilename
+Module._resolveFilename = function (request, parent) {
+  if (request.startsWith(json.name)) {
+    try {
+      return _resolveFilename(request, parent)
+    } catch (error) {
+      return require.resolve(request.replace(request.startsWith(json.name + '/') ? json.name : json.name.substring(0, json.name.indexOf('/')), '..'))
+    }
+  } else {
+    return _resolveFilename(request, parent)
+  }
+}
+
+if (argv._.length !== 1 || path.extname(argv._[0]) !== '.js') {
+  console.error('Usage:')
+  console.error('  compose composition.js [flags]')
+  console.error('Flags:')
+  console.error('  --ast                  only output the ast for the composition')
+  console.error('  -v, --version          output the composer version')
+  process.exit(1)
+}
+
+let composition
+try {
+  composition = composer.parse(require(path.resolve(argv._[0]))) // load and validate composition
+  composition = composition.compile()
+} catch (error) {
+  error.statusCode = 422
+  console.error(error)
+  process.exit(422 - 256) // Unprocessable Entity
+}
+if (argv.ast) composition = composition.ast
+console.log(JSON.stringify(composition, null, 4))
diff --git a/bin/deploy.js b/bin/deploy.js
new file mode 100755
index 0000000..bbd9bce
--- /dev/null
+++ b/bin/deploy.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+
+/*
+ * Copyright 2017-2018 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict'
+
+const composer = require('../composer')
+const conductor = require('../conductor')
+const fqn = require('openwhisk-fqn')
+const fs = require('fs')
+const json = require('../package.json')
+const minimist = require('minimist')
+const path = require('path')
+
+const argv = minimist(process.argv.slice(2), {
+  string: ['apihost', 'auth', 'source', 'annotation', 'annotation-file'],
+  boolean: ['insecure', 'version', 'overwrite'],
+  alias: { auth: 'u', insecure: 'i', version: 'v', annotation: 'a', 'annotation-file': 'A', overwrite: 'w' }
+})
+
+if (argv.version) {
+  console.log(json.version)
+  process.exit(0)
+}
+
+if (argv._.length !== 2 || path.extname(argv._[1]) !== '.json') {
+  console.error('Usage:')
+  console.error('  deploy composition composition.json [flags]')
+  console.error('Flags:')
+  console.error('  -a, --annotation KEY=VALUE        add KEY annotation with VALUE')
+  console.error('  -A, --annotation-file KEY=FILE    add KEY annotation with FILE content')
+  console.error('  --apihost HOST                    API HOST')
+  console.error('  -i, --insecure                    bypass certificate checking')
+  console.error('  -u, --auth KEY                    authorization KEY')
+  console.error('  -v, --version                     output the composer version')
+  console.error('  -w, --overwrite                   overwrite actions if already defined')
+  process.exit(1)
+}
+let composition
+try {
+  composition = JSON.parse(fs.readFileSync(argv._[1], 'utf8'))
+  if (typeof composition !== 'object') throw new Error('Composition must be a dictionary')
+  if (typeof composition.ast !== 'object') throw new Error('Composition must have a field "ast" of type dictionary')
+  if (typeof composition.composition !== 'object') throw new Error('Composition must have a field "composition" of type dictionary')
+  if (typeof composition.version !== 'string') throw new Error('Composition must have a field "version" of type string')
+  if (composition.actions !== undefined && !Array.isArray(composition.actions)) throw new Error('Optional field "actions" must be an array')
+  composition.composition = composer.parse(composition.composition) // validate composition
+  if (typeof argv.annotation === 'string') argv.annotation = [argv.annotation]
+  composition.annotations = []
+  for (let annotation of [...(argv.annotation || [])]) {
+    const index = annotation.indexOf('=')
+    if (index < 0) throw Error('Annotation syntax must be "KEY=VALUE"')
+    composition.annotations.push({ key: annotation.substring(0, index), value: annotation.substring(index + 1) })
+  }
+  if (typeof argv['annotation-file'] === 'string') argv['annotation-file'] = [argv['annotation-file']]
+  for (let annotation of argv['annotation-file'] || []) {
+    const index = annotation.indexOf('=')
+    if (index < 0) throw Error('Annotation syntax must be "KEY=FILE"')
+    composition.annotations.push({ key: annotation.substring(0, index), value: fs.readFileSync(annotation.substring(index + 1), 'utf8') })
+  }
+} catch (error) {
+  error.statusCode = 422
+  console.error(error)
+  process.exit(422 - 256) // Unprocessable Entity
+}
+const options = { ignore_certs: argv.insecure }
+if (argv.apihost) options.apihost = argv.apihost
+if (argv.auth) options.api_key = argv.auth
+try {
+  composition.name = fqn(argv._[0])
+} catch (error) {
+  error.statusCode = 400
+  console.error(error)
+  process.exit(400 - 256) // Bad Request
+}
+conductor(options).compositions.deploy(composition, argv.overwrite)
+  .then(actions => {
+    const names = actions.map(action => action.name)
+    console.log(`ok: created action${actions.length > 1 ? 's' : ''} ${names}`)
+  })
+  .catch(error => {
+    error.statusCode = error.statusCode || 500
+    console.error(error)
+    process.exit(error.statusCode - 256)
+  })
diff --git a/composer.js b/composer.js
index b1b0ac9..e86af89 100644
--- a/composer.js
+++ b/composer.js
@@ -16,708 +16,355 @@
 
 'use strict'
 
+const fqn = require('openwhisk-fqn')
 const fs = require('fs')
-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)
 
-// combinator signatures
-const combinators = {}
-
 // error class
 class ComposerError extends Error {
-    constructor(message, argument) {
-        super(message + (argument !== undefined ? '\nArgument: ' + util.inspect(argument) : ''))
-    }
+  constructor (message, argument) {
+    super(message + (argument !== undefined ? '\nArgument value: ' + util.inspect(argument) : ''))
+  }
 }
 
-// registered plugins
-const plugins = []
-
-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 composer.empty()
-        if (task instanceof Composition) return task
-        if (typeof task === 'function') return composer.function(task)
-        if (typeof task === 'string') return composer.action(task)
-        throw new ComposerError('Invalid argument', task)
-    },
-
-    // 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 (!isObject(fun)) throw new ComposerError('Invalid argument', fun)
-        return new Composition({ type: 'function', function: { exec: fun } })
-    },
-
-    // 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 = 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(canonical) }
-        } else if (typeof options.filename === 'string') { // read action code from file
-            exec = fs.readFileSync(options.filename, { encoding: 'utf8' })
-        } 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)
-        } else if (typeof options.action === 'string' || isObject(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)
-    },
-})
+const composer = { util: { declare, version } }
 
 const lowerer = {
-    empty() {
-        return composer.sequence()
-    },
+  literal (value) {
+    return composer.let({ value }, () => value)
+  },
 
-    seq({ components }) {
-        return composer.sequence(...components)
-    },
+  retain (...components) {
+    let params = null
+    return composer.let(
+      { params },
+      composer.finally(
+        args => { params = args },
+        composer.seq(composer.mask(...components),
+          result => ({ params, result }))))
+  },
 
-    value({ value }) {
-        return composer.literal(value)
-    },
+  retain_catch (...components) {
+    return composer.seq(
+      composer.retain(
+        composer.finally(
+          composer.seq(...components),
+          result => ({ result }))),
+      ({ params, result }) => ({ params, result: result.result }))
+  },
 
-    literal({ value }) {
-        return composer.let({ value }, composer.function('() => value'))
-    },
+  if (test, consequent, alternate) {
+    let params = null
+    return composer.let(
+      { params },
+      composer.finally(
+        args => { params = args },
+        composer.if_nosave(
+          composer.mask(test),
+          composer.finally(() => params, composer.mask(consequent)),
+          composer.finally(() => params, composer.mask(alternate)))))
+  },
 
-    retain({ components }) {
-        return composer.let(
-            { params: null },
-            composer.finally(
-                args => { params = args },
-                composer.seq(composer.mask(...components),
-                    result => ({ params, result }))))
-    },
+  while (test, body) {
+    let params = null
+    return composer.let(
+      { 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)))
+  },
 
-    retain_catch({ components }) {
-        return composer.seq(
-            composer.retain(
-                composer.finally(
-                    composer.seq(...components),
-                    result => ({ result }))),
-            ({ params, result }) => ({ params, result: result.result }))
-    },
+  dowhile (body, test) {
+    let params = null
+    return composer.let(
+      { 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)))
+  },
 
-    if({ test, consequent, alternate }) {
-        return composer.let(
-            { params: null },
-            composer.finally(
-                args => { params = args },
-                composer.if_nosave(
-                    composer.mask(test),
-                    composer.finally(() => params, composer.mask(consequent)),
-                    composer.finally(() => params, composer.mask(alternate)))))
-    },
+  repeat (count, ...components) {
+    return composer.let(
+      { count },
+      composer.while(
+        () => count-- > 0,
+        composer.mask(...components)))
+  },
 
-    while({ test, body }) {
-        return composer.let(
-            { params: null },
-            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)))
-    },
+  retry (count, ...components) {
+    return composer.let(
+      { count },
+      params => ({ params }),
+      composer.dowhile(
+        composer.finally(({ params }) => params, composer.mask(composer.retain_catch(...components))),
+        ({ result }) => result.error !== undefined && count-- > 0),
+      ({ result }) => result)
+  },
 
-    dowhile({ body, test }) {
-        return composer.let(
-            { params: null },
-            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({ count, components }) {
-        return composer.let(
-            { count },
-            composer.while(
-                composer.function('() => count-- > 0'),
-                composer.mask(...components)))
-    },
-
-    retry({ count, components }) {
-        return composer.let(
-            { count },
-            params => ({ params }),
-            composer.dowhile(
-                composer.finally(({ params }) => params, composer.mask(composer.retain_catch(...components))),
-                composer.function('({ result }) => result.error !== undefined && count-- > 0')),
-            ({ result }) => result)
-    },
+  merge (...components) {
+    return composer.seq(composer.retain(...components), ({ params, result }) => Object.assign(params, result))
+  }
 }
 
-// recursively compile composition composition into { composition, actions }
-function flatten(composition) {
-    if (arguments.length > 1) throw new ComposerError('Too many arguments')
-    if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
+// apply f to all fields of type composition
+function visit (composition, f) {
+  composition = Object.assign({}, composition) // copy
+  const combinator = composition['.combinator']()
+  if (combinator.components) {
+    composition.components = composition.components.map(f)
+  }
+  for (let arg of combinator.args || []) {
+    if (arg.type === undefined && composition[arg.name] !== undefined) {
+      composition[arg.name] = f(composition[arg.name], arg.name)
+    }
+  }
+  return new Composition(composition)
+}
+
+// recursively label combinators with the json path
+function label (composition) {
+  const label = path => (composition, name, array) => {
+    const p = path + (name !== undefined ? (array === undefined ? `.${name}` : `[${name}]`) : '')
+    composition = visit(composition, label(p)) // copy
+    composition.path = p
+    return composition
+  }
+  return label('')(composition)
+}
+
+// derive combinator methods from combinator table
+// check argument count and map argument positions to argument names
+// delegate to Composition constructor for the rest of the validation
+function declare (combinators, prefix) {
+  if (arguments.length > 2) throw new ComposerError('Too many arguments in "declare"')
+  if (!isObject(combinators)) throw new ComposerError('Invalid argument "combinators" in "declare"', combinators)
+  if (prefix !== undefined && typeof prefix !== 'string') throw new ComposerError('Invalid argument "prefix" in "declare"', prefix)
+  const composer = {}
+  for (let key in combinators) {
+    const type = prefix ? prefix + '.' + key : key
+    const combinator = combinators[key]
+    if (!isObject(combinator) || (combinator.args !== undefined && !Array.isArray(combinator.args))) {
+      throw new ComposerError(`Invalid "${type}" combinator specification in "declare"`, combinator)
+    }
+    for (let arg of combinator.args || []) {
+      if (typeof arg.name !== 'string') throw new ComposerError(`Invalid "${type}" combinator specification in "declare"`, combinator)
+    }
+    composer[key] = function () {
+      const composition = { type, '.combinator': () => combinator }
+      const skip = (combinator.args && combinator.args.length) || 0
+      if (!combinator.components && (arguments.length > skip)) {
+        throw new ComposerError(`Too many arguments in "${type}" combinator`)
+      }
+      for (let i = 0; i < skip; ++i) {
+        composition[combinator.args[i].name] = arguments[i]
+      }
+      if (combinator.components) {
+        composition.components = Array.prototype.slice.call(arguments, skip)
+      }
+      return new Composition(composition)
+    }
+  }
+  return composer
+}
+
+// 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) {
+    const combinator = composition['.combinator']()
+    Object.assign(this, composition)
+    for (let arg of combinator.args || []) {
+      if (composition[arg.name] === undefined && arg.optional && arg.type !== undefined) continue
+      switch (arg.type) {
+        case undefined:
+          try {
+            this[arg.name] = composer.task(arg.optional ? composition[arg.name] || null : composition[arg.name])
+          } catch (error) {
+            throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name])
+          }
+          break
+        case 'name':
+          try {
+            this[arg.name] = fqn(composition[arg.name])
+          } catch (error) {
+            throw new ComposerError(`${error.message} in "${composition.type} combinator"`, composition[arg.name])
+          }
+          break
+        case 'value':
+          if (typeof composition[arg.name] === 'function' || composition[arg.name] === undefined) {
+            throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name])
+          }
+          break
+        case 'object':
+          if (!isObject(composition[arg.name])) {
+            throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name])
+          }
+          break
+        default:
+          if ('' + typeof composition[arg.name] !== arg.type) {
+            throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name])
+          }
+      }
+    }
+    if (combinator.components) this.components = (composition.components || []).map(obj => composer.task(obj))
+    return this
+  }
+
+  // compile composition
+  compile () {
+    if (arguments.length > 0) throw new ComposerError('Too many arguments in "compile"')
 
     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 = visit(composition, 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 }
+    const obj = { composition: label(flatten(this)).lower(), ast: this, version }
+    if (actions.length > 0) obj.actions = actions
+    return obj
+  }
+
+  // recursively lower combinators to the desired set of combinators (including primitive combinators)
+  lower (combinators = []) {
+    if (arguments.length > 1) throw new ComposerError('Too many arguments in "lower"')
+    if (!Array.isArray(combinators)) throw new ComposerError('Invalid argument "combinators" in "lower"', combinators)
+
+    const lower = composition => {
+      // repeatedly lower root combinator
+      while (composition['.combinator']().def) {
+        const path = composition.path
+        const combinator = composition['.combinator']()
+        if (Array.isArray(combinators) && combinators.indexOf(composition.type) >= 0) break
+        // map argument names to positions
+        const args = []
+        const skip = (combinator.args && combinator.args.length) || 0
+        for (let i = 0; i < skip; i++) args.push(composition[combinator.args[i].name])
+        if (combinator.components) args.push(...composition.components)
+        composition = combinator.def(...args)
+        if (path !== undefined) composition.path = path // preserve path
+      }
+      // lower nested combinators
+      return visit(composition, lower)
+    }
+
+    return lower(this)
+  }
 }
 
-// synthesize conductor action code from composition
-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})(`
-    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${composer.util.version}\n\nconst composition = ${JSON.stringify(composer.util.lower(label(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 }] }
+// primitive combinators
+const combinators = {
+  sequence: { components: true },
+  if_nosave: { args: [{ name: 'test' }, { name: 'consequent' }, { name: 'alternate', optional: true }] },
+  while_nosave: { args: [{ name: 'test' }, { name: 'body' }] },
+  dowhile_nosave: { args: [{ name: 'body' }, { name: 'test' }] },
+  try: { args: [{ name: 'body' }, { name: 'handler' }] },
+  finally: { args: [{ name: 'body' }, { name: 'finalizer' }] },
+  let: { args: [{ name: 'declarations', type: 'object' }], components: true },
+  mask: { components: true },
+  action: { args: [{ name: 'name', type: 'name' }, { name: 'action', type: 'object', optional: true }] },
+  function: { args: [{ name: 'function', type: 'object' }] },
+  async: { components: true }
 }
 
-composer.util = {
-    // return the signatures of the combinators
-    get combinators() {
-        return combinators
-    },
+Object.assign(composer, declare(combinators))
 
-    // recursively deserialize composition
-    deserialize(composition) {
-        if (arguments.length > 1) throw new ComposerError('Too many arguments')
-        composition = new Composition(composition) // copy
-        composition.visit(composition => composer.util.deserialize(composition))
-        return composition
-    },
-
-    // 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 (typeof combinators === 'string') { // lower to combinators of specific composer version 
-            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 && lowerer[composition.type]) {
-                const path = composition.path
-                composition = lowerer[composition.type](composition)
-                if (path !== undefined) composition.path = path // preserve path
-            }
-            // lower nested combinators
-            composition.visit(lower)
-            return composition
-        }
-
-        return lower(composition)
-    },
-
-    // register plugin
-    register(plugin) {
-        plugins.push(plugin)
-    },
-
-    /**
-     * 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
-    },
-
-    // derive combinator methods from combinator table
-    declare(combinators) {
-        Object.assign(composer.util.combinators, combinators)
-        for (let type in combinators) {
-            const combinator = combinators[type]
-            // do not overwrite existing combinators
-            composer[type] = composer[type] || function () {
-                const 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 = arguments[i]
-                    if (argument === undefined && arg.optional && arg.type !== undefined) continue
-                    switch (arg.type) {
-                        case undefined:
-                            composition[arg._] = composer.task(arg.optional ? argument || null : 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 => composer.task(obj))
-                }
-                return new Composition(composition)
-            }
-        }
-    },
-
-    get lowerer() {
-        return lowerer
-    },
+// derived combinators
+const extra = {
+  empty: { def: composer.sequence },
+  seq: { components: true, def: composer.sequence },
+  if: { args: [{ name: 'test' }, { name: 'consequent' }, { name: 'alternate', optional: true }], def: lowerer.if },
+  while: { args: [{ name: 'test' }, { name: 'body' }], def: lowerer.while },
+  dowhile: { args: [{ name: 'body' }, { name: 'test' }], def: lowerer.dowhile },
+  repeat: { args: [{ name: 'count', type: 'number' }], components: true, def: lowerer.repeat },
+  retry: { args: [{ name: 'count', type: 'number' }], components: true, def: lowerer.retry },
+  retain: { components: true, def: lowerer.retain },
+  retain_catch: { components: true, def: lowerer.retain_catch },
+  value: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal },
+  literal: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal },
+  merge: { components: true, def: lowerer.merge }
 }
 
-composer.util.frontend = composer.util.openwhisk
+Object.assign(composer, declare(extra))
 
-// 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
+// add or override definitions of some combinators
+Object.assign(composer, {
+  // detect task type and create corresponding composition object
+  task (task) {
+    if (arguments.length > 1) throw new ComposerError('Too many arguments in "task" combinator')
+    if (task === undefined) throw new ComposerError('Invalid argument in "task" combinator', task)
+    if (task === null) return composer.empty()
+    if (task instanceof Composition) return task
+    if (typeof task === 'function') return composer.function(task)
+    if (typeof task === 'string') return composer.action(task)
+    throw new ComposerError('Invalid argument "task" in "task" combinator', task)
+  },
+
+  // function combinator: stringify function code
+  function (fun) {
+    if (arguments.length > 1) throw new ComposerError('Too many arguments in "function" combinator')
+    if (typeof fun === 'function') {
+      fun = `${fun}`
+      if (fun.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function in "function" combinator', fun)
     }
-
-    // construct a composition object with the specified fields
-    constructor(composition) {
-        if (!isObject(composition) || composer.util.combinators[composition.type] === undefined) throw new ComposerError('Invalid argument', composition)
-        const combinator = composer.util.combinators[composition.type]
-        if (combinator.components && composition.components === undefined) throw new ComposerError('Invalid argument', composition)
-        for (let arg of combinator.args || []) {
-            if (!arg.optional && composition[arg._] === undefined) throw new ComposerError('Invalid argument', composition)
-        }
-        return Object.assign(this, composition)
+    if (typeof fun === 'string') {
+      fun = { kind: 'nodejs:default', code: fun }
     }
+    if (!isObject(fun)) throw new ComposerError('Invalid argument "function" in "function" combinator', fun)
+    return new Composition({ type: 'function', function: { exec: fun }, '.combinator': () => combinators.function })
+  },
 
-    // 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._)
-            }
-        }
+  // action combinator
+  action (name, options = {}) {
+    if (arguments.length > 2) throw new ComposerError('Too many arguments in "action" combinator')
+    if (!isObject(options)) throw new ComposerError('Invalid argument "options" in "action" combinator', options)
+    let exec
+    if (Array.isArray(options.sequence)) { // native sequence
+      exec = { kind: 'sequence', components: options.sequence.map(fqn) }
+    } else if (typeof options.filename === 'string') { // read action code from file
+      exec = fs.readFileSync(options.filename, { encoding: 'utf8' })
+    } 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 in "action" combinator', options.action)
+    } else if (typeof options.action === 'string' || isObject(options.action)) {
+      exec = options.action
     }
-}
+    if (typeof exec === 'string') {
+      exec = { kind: 'nodejs:default', code: exec }
+    }
+    const composition = { type: 'action', name, '.combinator': () => combinators.action }
+    if (exec) composition.action = { exec }
+    return new Composition(composition)
+  },
 
-Composition.composer = composer
-
-composer.util.declare({
-    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' },
+  // recursively deserialize composition
+  parse (composition) {
+    if (arguments.length > 1) throw new ComposerError('Too many arguments in "parse" combinator')
+    if (!isObject(composition)) throw new ComposerError('Invalid argument "composition" in "parse" combinator', composition)
+    const combinator = typeof composition['.combinator'] === 'function' ? composition['.combinator']() : combinators[composition.type]
+    if (!isObject(combinator)) throw new ComposerError('Invalid composition type in "parse" combinator', composition)
+    return visit(Object.assign({ '.combinator': () => combinator }, composition), composition => composer.parse(composition))
+  }
 })
 
-// management class for compositions
-class Compositions {
-    constructor(wsk) {
-        this.actions = wsk.actions
-    }
-
-    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)
-    }
-}
-
-// 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)
-}
-
-// runtime code
-function main() {
-    const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj)
-
-    // compile ast to fsm
-    const compiler = {
-        sequence(node) {
-            return [{ type: 'pass', path: node.path }, ...compile(...node.components)]
-        },
-
-        action(node) {
-            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) {
-            return [{ type: 'function', exec: node.function.exec, path: node.path }]
-        },
-
-        finally(node) {
-            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) {
-            return [{ type: 'let', let: node.declarations, path: node.path }, ...compile(...node.components), { type: 'exit' }]
-        },
-
-        mask(node) {
-            return [{ type: 'let', let: null, path: node.path }, ...compile(...node.components), { type: 'exit' }]
-        },
-
-        try(node) {
-            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) {
-            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) {
-            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) {
-            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
-
-    const conductor = {
-        choice({ p, node, index }) {
-            p.s.state = index + (p.params.value ? node.then : node.else)
-        },
-
-        try({ p, node, index }) {
-            p.s.stack.unshift({ catch: index + node.catch })
-        },
-
-        let({ p, node, index }) {
-            p.s.stack.unshift({ let: JSON.parse(JSON.stringify(node.let)) })
-        },
-
-        exit({ p, node, index }) {
-            if (p.s.stack.length === 0) return internalError(`State ${index} attempted to pop from an empty stack`)
-            p.s.stack.shift()
-        },
-
-        action({ p, node, index }) {
-            return { action: node.name, params: p.params, state: { $resume: p.s } }
-        },
-
-        function({ p, node, index }) {
-            return Promise.resolve().then(() => run(node.exec.code, p))
-                .catch(error => {
-                    console.error(error)
-                    return { error: `An exception was caught at state ${index} (see log for details)` }
-                })
-                .then(result => {
-                    if (typeof result === 'function') result = { error: `State ${index} evaluated to a function` }
-                    // if a function has only side effects and no return value, return params
-                    p.params = JSON.parse(JSON.stringify(result === undefined ? p.params : result))
-                    inspect(p)
-                    return step(p)
-                })
-        },
-
-        empty({ p, node, index }) {
-            inspect(p)
-        },
-
-        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 (let { plugin, config } of arguments) {
-        if (plugin.compiler) Object.assign(compiler, plugin.compiler({ compile }))
-        if (plugin.conductor) {
-            Object.assign(conductor, plugin.conductor(config))
-            if (conductor._finish) {
-                finishers.push(conductor._finish)
-                delete conductor._finish
-            }
-        }
-    }
-
-    const fsm = compile(composition)
-
-    // encode error object
-    const encodeError = error => ({
-        code: typeof error.code === 'number' && error.code || 500,
-        error: (typeof error.error === 'string' && error.error) || error.message || (typeof error === 'string' && error) || 'An internal error occurred'
-    })
-
-    // error status codes
-    const badRequest = error => Promise.reject({ code: 400, error })
-    const internalError = error => Promise.reject(encodeError(error))
-
-    // wrap params if not a dictionary, branch to error handler if error
-    function inspect(p) {
-        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 = -1 // abort unless there is a handler in the stack
-            while (p.s.stack.length > 0) {
-                if ((p.s.state = p.s.stack.shift().catch || -1) >= 0) break
-            }
-        }
-    }
-
-    // run function f on current stack
-    function run(f, p) {
-        // handle let/mask pairs
-        const view = []
-        let n = 0
-        for (let frame of p.s.stack) {
-            if (frame.let === null) {
-                n++
-            } else if (frame.let !== undefined) {
-                if (n === 0) {
-                    view.push(frame)
-                } else {
-                    n--
-                }
-            }
-        }
-
-        // update value of topmost matching symbol on stack if any
-        function set(symbol, 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
-        const env = view.reduceRight((acc, cur) => cur.let ? Object.assign(acc, cur.let) : acc, {})
-        let main = '(function(){try{'
-        for (const name in env) main += `var ${name}=arguments[1]['${name}'];`
-        main += `return eval((${f}))(arguments[0])}finally{`
-        for (const name in env) main += `arguments[1]['${name}']=${name};`
-        main += '}})'
-        try {
-            return (1, eval)(main)(p.params, env)
-        } finally {
-            for (const name in env) set(name, env[name])
-        }
-    }
-
-    function step(p) {
-        // final state, return composition result
-        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())
-                .then(() => p.params.error ? p.params : { params: p.params })
-        }
-
-        // 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 // 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)
-    }
-
-    return params => Promise.resolve().then(() => invoke(params)).catch(internalError)
-
-    // do invocation
-    function invoke(params) {
-        const p = { s: { state: 0, stack: [] }, params } // initial state
-
-        if (params.$resume !== undefined) {
-            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 (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
-        }
-
-        return step(p)
-    }
-}
-
 module.exports = composer
diff --git a/conductor.js b/conductor.js
new file mode 100644
index 0000000..bac9641
--- /dev/null
+++ b/conductor.js
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2017-2018 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint no-eval: 0 */
+
+'use strict'
+
+const fs = require('fs')
+const { minify } = require('terser')
+const openwhisk = require('openwhisk')
+const os = require('os')
+const path = require('path')
+
+// read conductor version number
+const version = require('./package.json').version
+
+// synthesize conductor action code from composition
+function synthesize ({ name, composition, ast, version: composer, annotations = [] }) {
+  const code = `// generated by composer v${composer} and conductor v${version}\n\nconst composition = ${JSON.stringify(composition, null, 4)}\n\n// do not edit below this point\n\n` +
+    minify(`const main=(${main})(composition)`, { output: { max_line_len: 127 } }).code
+  annotations = annotations.concat([{ key: 'conductor', value: ast }, { key: 'composerVersion', value: composer }, { key: 'conductorVersion', value: version }])
+  return { name, action: { exec: { kind: 'nodejs:default', code }, annotations } }
+}
+
+// return enhanced openwhisk client capable of deploying compositions
+module.exports = function (options) {
+  // try to extract apihost and key first from whisk property file file and then from process.env
+  let apihost
+  let apikey
+
+  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') {
+          apikey = parts[1]
+        }
+      }
+    }
+  } catch (error) { }
+
+  if (process.env.__OW_API_HOST) apihost = process.env.__OW_API_HOST
+  if (process.env.__OW_API_KEY) apikey = process.env.__OW_API_KEY
+
+  const wsk = openwhisk(Object.assign({ apihost, api_key: apikey }, options))
+  wsk.compositions = new Compositions(wsk)
+  return wsk
+}
+
+// management class for compositions
+class Compositions {
+  constructor (wsk) {
+    this.actions = wsk.actions
+  }
+
+  deploy (composition, overwrite) {
+    const actions = (composition.actions || []).concat(synthesize(composition))
+    return actions.reduce((promise, action) => promise.then(() => overwrite && this.actions.delete(action).catch(() => { }))
+      .then(() => this.actions.create(action)), Promise.resolve())
+      .then(() => actions)
+  }
+}
+
+// runtime code
+function main (composition) {
+  const openwhisk = require('openwhisk')
+  let wsk
+
+  const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj)
+
+  // compile ast to fsm
+  const compiler = {
+    sequence (parent, node) {
+      return [{ parent, type: 'pass' }, ...compile(parent, ...node.components)]
+    },
+
+    action (parent, node) {
+      return [{ parent, type: 'action', name: node.name }]
+    },
+
+    async (parent, node) {
+      const body = [...compile(parent, ...node.components)]
+      return [{ parent, type: 'async', return: body.length + 2 }, ...body, { parent, type: 'stop' }, { parent, type: 'pass' }]
+    },
+
+    function (parent, node) {
+      return [{ parent, type: 'function', exec: node.function.exec }]
+    },
+
+    finally (parent, node) {
+      const finalizer = compile(parent, node.finalizer)
+      const fsm = [{ parent, type: 'try' }, ...compile(parent, node.body), { parent, type: 'exit' }, ...finalizer]
+      fsm[0].catch = fsm.length - finalizer.length
+      return fsm
+    },
+
+    let (parent, node) {
+      return [{ parent, type: 'let', let: node.declarations }, ...compile(parent, ...node.components), { parent, type: 'exit' }]
+    },
+
+    mask (parent, node) {
+      return [{ parent, type: 'let', let: null }, ...compile(parent, ...node.components), { parent, type: 'exit' }]
+    },
+
+    try (parent, node) {
+      const handler = [...compile(parent, node.handler), { parent, type: 'pass' }]
+      const fsm = [{ parent, type: 'try' }, ...compile(parent, node.body), { parent, type: 'exit' }, ...handler]
+      fsm[0].catch = fsm.length - handler.length
+      fsm[fsm.length - handler.length - 1].next = handler.length
+      return fsm
+    },
+
+    if_nosave (parent, node) {
+      const consequent = compile(parent, node.consequent)
+      const alternate = [...compile(parent, node.alternate), { parent, type: 'pass' }]
+      const fsm = [{ parent, type: 'pass' }, ...compile(parent, node.test), { parent, type: 'choice', then: 1, else: consequent.length + 1 }, ...consequent, ...alternate]
+      fsm[fsm.length - alternate.length - 1].next = alternate.length
+      return fsm
+    },
+
+    while_nosave (parent, node) {
+      const body = compile(parent, node.body)
+      const fsm = [{ parent, type: 'pass' }, ...compile(parent, node.test), { parent, type: 'choice', then: 1, else: body.length + 1 }, ...body, { parent, type: 'pass' }]
+      fsm[fsm.length - 2].next = 2 - fsm.length
+      return fsm
+    },
+
+    dowhile_nosave (parent, node) {
+      const fsm = [{ parent, type: 'pass' }, ...compile(parent, node.body), ...compile(parent, node.test), { parent, type: 'choice', else: 1 }, { parent, type: 'pass' }]
+      fsm[fsm.length - 2].then = 2 - fsm.length
+      return fsm
+    }
+  }
+
+  function compile (parent, node) {
+    if (arguments.length === 1) return [{ parent, type: 'empty' }]
+    if (arguments.length === 2) return Object.assign(compiler[node.type](node.path || parent, node), { path: node.path })
+    return Array.prototype.slice.call(arguments, 1).reduce((fsm, node) => { fsm.push(...compile(parent, node)); return fsm }, [])
+  }
+
+  const fsm = compile('', composition)
+
+  const conductor = {
+    choice ({ p, node, index }) {
+      p.s.state = index + (p.params.value ? node.then : node.else)
+    },
+
+    try ({ p, node, index }) {
+      p.s.stack.unshift({ catch: index + node.catch })
+    },
+
+    let ({ p, node, index }) {
+      p.s.stack.unshift({ let: JSON.parse(JSON.stringify(node.let)) })
+    },
+
+    exit ({ p, node, index }) {
+      if (p.s.stack.length === 0) return internalError(`pop from an empty stack`)
+      p.s.stack.shift()
+    },
+
+    action ({ p, node, index }) {
+      return { method: 'action', action: node.name, params: p.params, state: { $resume: p.s } }
+    },
+
+    function ({ p, node, index }) {
+      return Promise.resolve().then(() => run(node.exec.code, p))
+        .catch(error => {
+          console.error(error)
+          return { error: `Function combinator threw an exception at AST node root${node.parent} (see log for details)` }
+        })
+        .then(result => {
+          if (typeof result === 'function') result = { error: `Function combinator evaluated to a function type at AST node root${node.parent}` }
+          // if a function has only side effects and no return value, return params
+          p.params = JSON.parse(JSON.stringify(result === undefined ? p.params : result))
+          inspect(p)
+          return step(p)
+        })
+    },
+
+    empty ({ p, node, index }) {
+      inspect(p)
+    },
+
+    pass ({ p, node, index }) {
+    },
+
+    async ({ p, node, index, inspect, step }) {
+      p.params.$resume = { state: p.s.state, stack: [{ marker: true }].concat(p.s.stack) }
+      p.s.state = index + node.return
+      if (!wsk) wsk = openwhisk({ ignore_certs: true })
+      return wsk.actions.invoke({ name: process.env.__OW_ACTION_NAME, params: p.params })
+        .then(response => ({ method: 'async', activationId: response.activationId, sessionId: p.s.session }), error => {
+          console.error(error) // invoke failed
+          return { error: `Async combinator failed to invoke composition at AST node root${node.parent} (see log for details)` }
+        })
+        .then(result => {
+          p.params = result
+          inspect(p)
+          return step(p)
+        })
+    },
+
+    stop ({ p, node, index, inspect, step }) {
+      p.s.state = -1
+    }
+  }
+
+  function finish (p) {
+    return p.params.error ? p.params : { params: p.params }
+  }
+
+  const internalError = error => Promise.reject(error) // terminate composition execution and record error
+
+  // wrap params if not a dictionary, branch to error handler if error
+  function inspect (p) {
+    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 = -1 // abort unless there is a handler in the stack
+      while (p.s.stack.length > 0 && !p.s.stack[0].marker) {
+        if ((p.s.state = p.s.stack.shift().catch || -1) >= 0) break
+      }
+    }
+  }
+
+  // run function f on current stack
+  function run (f, p) {
+    // handle let/mask pairs
+    const view = []
+    let n = 0
+    for (let frame of p.s.stack) {
+      if (frame.let === null) {
+        n++
+      } else if (frame.let !== undefined) {
+        if (n === 0) {
+          view.push(frame)
+        } else {
+          n--
+        }
+      }
+    }
+
+    // update value of topmost matching symbol on stack if any
+    function set (symbol, 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
+    const env = view.reduceRight((acc, cur) => cur.let ? Object.assign(acc, cur.let) : acc, {})
+    let main = '(function(){try{const require=arguments[2];'
+    for (const name in env) main += `var ${name}=arguments[1]['${name}'];`
+    main += `return eval((function(){return(${f})})())(arguments[0])}finally{`
+    for (const name in env) main += `arguments[1]['${name}']=${name};`
+    main += '}})'
+    try {
+      return (1, eval)(main)(p.params, env, require)
+    } finally {
+      for (const name in env) set(name, env[name])
+    }
+  }
+
+  function step (p) {
+    // final state, return composition result
+    if (p.s.state < 0 || p.s.state >= fsm.length) {
+      console.log(`Entering final state`)
+      console.log(JSON.stringify(p.params))
+      return
+    }
+
+    // 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 // current state
+    p.s.state = p.s.state + (node.next || 1) // default next state
+    if (typeof conductor[node.type] !== 'function') return internalError(`unexpected "${node.type}" combinator`)
+    return conductor[node.type]({ p, index, node, inspect, step }) || step(p)
+  }
+
+  // do invocation
+  return (params) => {
+    // extract parameters
+    const $resume = params.$resume || {}
+    delete params.$resume
+    $resume.session = $resume.session || process.env.__OW_ACTIVATION_ID
+
+    // current state
+    const p = { s: Object.assign({ state: 0, stack: [], resuming: true }, $resume), params }
+
+    // step and catch all errors
+    return Promise.resolve().then(() => {
+      if (typeof p.s.state !== 'number') return internalError('state parameter is not a number')
+      if (!Array.isArray(p.s.stack)) return internalError('stack parameter is not an array')
+
+      if ($resume.resuming) inspect(p) // handle error objects when resuming
+
+      return step(p)
+    }).catch(error => {
+      const message = (typeof error.error === 'string' && error.error) || error.message || (typeof error === 'string' && error)
+      p.params = { error: message ? `Internal error: ${message}` : 'Internal error' }
+    }).then(params => params || finish(p)) // params is defined iff execution will be resumed
+  }
+}
diff --git a/docs/COMBINATORS.md b/docs/COMBINATORS.md
index 83af8c9..b4d29fc 100644
--- a/docs/COMBINATORS.md
+++ b/docs/COMBINATORS.md
@@ -4,25 +4,28 @@
 
 | Combinator | Description | Example |
 | --:| --- | --- | 
-| [`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!' })` |
+| [`action`](#action) | named action | `composer.action('echo')` |
+| [`async`](#async) | asynchronous invocation | `composer.async('compress', 'upload')` |
+| [`dowhile` and `dowhile_nosave`](#dowhile) | loop at least once | `composer.dowhile('fetchData', 'needMoreData')` |
 | [`empty`](#empty) | empty sequence | `composer.empty()`
+| [`finally`](#finally) | finalization | `composer.finally('tryThis', 'doThatAlways')` |
+| [`function`](#function) | Javascript function | `composer.function(({ x, y }) => ({ product: x * y }))` |
+| [`if` and `if_nosave`](#if) | conditional | `composer.if('authenticate', 'success', 'failure')` |
+| [`let`](#let) | variable declarations | `composer.let({ count: 3, message: 'hello' }, ...)` |
+| [`literal` or `value`](#literal) | constant value | `composer.literal({ message: 'Hello, World!' })` |
+| [`mask`](#mask) | variable hiding | `composer.let({ n }, composer.while(_ => n-- > 0, composer.mask(composition)))` |
+| [`merge`](#merge) | data augmentation | `composer.merge('hash')` |
+| [`repeat`](#repeat) | counted loop | `composer.repeat(3, 'hello')` |
+| [`retain` and `retain_catch`](#retain) | persistence | `composer.retain('validateInput')` |
+| [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` |
 | [`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')` |
-| [`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` and `retain_catch`](#retain) | persistence | `composer.retain('validateInput')` |
-| [`async`](#async) | asynchronous invocation | `composer.async('sendMessage')` |
+| [`while` and `while_nosave`](#while) | loop | `composer.while('notEnough', 'doMore')` |
 
-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.
+The `action`, `function`, and `literal` combinators construct compositions
+respectively from OpenWhisk actions, Javascript functions, and constant values.
+The other combinators combine existing compositions to produce new compositions.
 
 ## Shorthands
 
@@ -31,15 +34,16 @@
  - `fun` of type `function` stands for `composer.function(fun)`,
  - `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.
-
 ## Action
 
-`composer.action(name, [options])` is a composition with a single action named _name_. It invokes the action named _name_ on the input parameter object for the composition and returns the output parameter object of this action invocation.
+`composer.action(name, [options])` is a composition with a single action named
+_name_. It invokes the action named _name_ on the input parameter object for the
+composition and returns the output parameter object of this action invocation.
 
-The action _name_ may specify the namespace and/or package containing the action following the usual OpenWhisk grammar. If no namespace is specified, the default namespace is assumed. If no package is specified, the default package is assumed.
+The action _name_ may specify the namespace and/or package containing the action
+following the usual OpenWhisk grammar. If no namespace is specified, the default
+namespace is assumed. If no package is specified, the default package is
+assumed.
 
 Examples:
 ```javascript
@@ -47,7 +51,8 @@
 composer.action('myPackage/myAction')
 composer.action('/whisk.system/utils/echo')
 ```
-The optional `options` dictionary makes it possible to provide a definition for the action being composed.
+The optional `options` dictionary makes it possible to provide a definition for
+the action being composed.
 ```javascript
 // specify the code for the action as a function
 composer.action('hello', { action: function () { return { message: 'hello' } } })
@@ -76,16 +81,22 @@
 // specify a sequence of actions
 composer.action('helloAndBye', { sequence: ['hello', 'bye'] })
 ```
-The action may be defined by providing the code for the action as a string, as a Javascript function, or as a file name. Alternatively, a sequence action may be defined by providing the list of sequenced actions. The code (specified as a string) may be annotated with the kind of the action runtime.
+The action may be defined by providing the code for the action as a string, as a
+Javascript function, or as a file name. Alternatively, a sequence action may be
+defined by providing the list of sequenced actions. The code (specified as a
+string) may be annotated with the kind of the action runtime.
 
 ### Environment capture in actions
 
-Javascript functions used to define actions cannot capture any part of their declaration environment. The following code is not correct as the declaration of `name` would not be available at invocation time:
+Javascript functions used to define actions cannot capture any part of their
+declaration environment. The following code is not correct as the declaration of
+`name` would not be available at invocation time:
 ```javascript
 let name = 'Dave'
 composer.action('hello', { action: function main() { return { message: 'Hello ' + name } } })
 ```
-In contrast, the following code is correct as it resolves `name`'s value at composition time.
+In contrast, the following code is correct as it resolves `name`'s value at
+composition time.
 ```javascript
 let name = 'Dave'
 composer.action('hello', { action: `function main() { return { message: 'Hello ' + '${name}' } }` })
@@ -93,11 +104,21 @@
 
 ## Function
 
-`composer.function(fun)` is a composition with a single Javascript function _fun_. It applies the specified function to the input parameter object for the composition.
- - If the function returns a value of type `function`, the composition returns an error object.
- - If the function throws an exception, the composition returns an error object. The exception is logged as part of the conductor action invocation.
- - If the function returns a value of type other than function, the value is first converted to a JSON value using `JSON.stringify` followed by `JSON.parse`. If the resulting JSON value is not a JSON dictionary, the JSON value is then wrapped into a `{ value }` dictionary. The composition returns the final JSON dictionary.
- - If the function does not return a value and does not throw an exception, the composition returns the input parameter object for the composition converted to a JSON dictionary using `JSON.stringify` followed by `JSON.parse`.
+`composer.function(fun)` is a composition with a single Javascript function
+_fun_. It applies the specified function to the input parameter object for the
+composition.
+ - If the function returns a value of type `function`, the composition returns
+   an error object.
+ - If the function throws an exception, the composition returns an error object.
+   The exception is logged as part of the conductor action invocation.
+ - If the function returns a value of type other than function, the value is
+   first converted to a JSON value using `JSON.stringify` followed by
+   `JSON.parse`. If the resulting JSON value is not a JSON dictionary, the JSON
+   value is then wrapped into a `{ value }` dictionary. The composition returns
+   the final JSON dictionary.
+ - If the function does not return a value and does not throw an exception, the
+   composition returns the input parameter object for the composition converted
+   to a JSON dictionary using `JSON.stringify` followed by `JSON.parse`.
 
 Examples:
 ```javascript
@@ -110,46 +131,74 @@
 
 ### Environment capture in functions
 
-Functions intended for compositions cannot capture any part of their declaration environment. They may however access and mutate variables in an environment consisting of the variables declared by the [composer.let](#composerletname-value-composition_1-composition_2-) combinator discussed below.
+Functions intended for compositions cannot capture any part of their declaration
+environment. They may however access and mutate variables in an environment
+consisting of the variables declared by the [let](#let) combinator discussed
+below.
 
-The following is not legal:
+The following code is not correct:
 ```javascript
 let name = 'Dave'
 composer.function(params => ({ message: 'Hello ' + name }))
 ```
-The following is legal:
+The following code is correct:
 ```javascript
 composer.let({ name: 'Dave' }, composer.function(params => ({ message: 'Hello ' + name })))
 ```
 
 ## Literal
 
-`composer.literal(value)` and its synonymous `composer.value(value)` output a constant JSON dictionary. This dictionary is obtained by first converting the _value_ argument to JSON using `JSON.stringify` followed by `JSON.parse`. If the resulting JSON value is not a JSON dictionary, the JSON value is then wrapped into a `{ value }` dictionary.
+`composer.literal(value)` and its synonymous `composer.value(value)` output a
+constant JSON dictionary. This dictionary is obtained by first converting the
+_value_ argument to JSON using `JSON.stringify` followed by `JSON.parse`. If the
+resulting JSON value is not a JSON dictionary, the JSON value is then wrapped
+into a `{ value }` dictionary.
 
-The _value_ argument may be computed at composition time. For instance, the following composition captures the date at the time the composition is encoded to JSON:
+The _value_ argument may be computed at composition time. For instance, the
+following composition captures the date at the time the composition is encoded
+to JSON:
 ```javascript
 composer.sequence(
     composer.literal(Date()),
     composer.action('log', { action: params => ({ message: 'Composition time: ' + params.value }) }))
 ```
 
-JSON values cannot represent functions. Applying `composer.literal` to a value of type `'function'` will result in an error. Functions embedded in a `value` of type `'object'`, e.g., `{ f: p => p, n: 42 }` will be silently omitted from the JSON dictionary. In other words, `composer.literal({ f: p => p, n: 42 })` will output `{ n: 42 }`.
+JSON values cannot represent functions. Applying `composer.literal` to a value
+of type `'function'` will result in an error. Functions embedded in a `value` of
+type `'object'`, e.g., `{ f: p => p, n: 42 }` will be silently omitted from the
+JSON dictionary. In other words, `composer.literal({ f: p => p, n: 42 })` will
+output `{ n: 42 }`.
 
-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.
-
-## 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.
+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.
 
 ## Sequence
 
-`composer.sequence(composition_1, composition_2, ...)` chains a series of compositions (possibly empty).
+`composer.sequence(composition_1, composition_2, ...)` or it synonymous
+`composer.seq(composition_1, composition_2, ...)` chain a series of compositions
+(possibly empty).
 
-The input parameter object for the composition is the input parameter object of the first composition in the sequence. The output parameter object of one composition in the sequence is the input parameter object for the next composition in the sequence. The output parameter object of the last composition in the sequence is the output parameter object for the composition.
+The input parameter object for the composition is the input parameter object of
+the first composition in the sequence. The output parameter object of one
+composition in the sequence is the input parameter object for the next
+composition in the sequence. The output parameter object of the last composition
+in the sequence is the output parameter object for the composition.
 
-If one of the components fails (i.e., returns an error object), the remainder of the sequence is not executed. The output parameter object for the composition is the error object produced by the failed component.
+If one of the components fails (i.e., returns an error object), the remainder of
+the sequence is not executed. The output parameter object for the composition is
+the error object produced by the failed component.
 
-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.
+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.
+
+## 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.
 
 ## Task
 
@@ -157,108 +206,203 @@
 
 ## 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.
+`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.
 
-The initial values must be valid JSON values. In particular, `composer.let({ foo: undefined })` is incorrect as `undefined` is not representable by a JSON value. On the other hand, `composer.let({ foo: null })` is correct. For the same reason, initial values cannot be functions, e.g., `composer.let({ foo: params => params })` is incorrect.
+The initial values must be valid JSON values. In particular, `composer.let({foo:
+undefined }, composition)` is incorrect as `undefined` is not representable by a
+JSON value. Use `composer.let({ foo: null }, composition)` instead. For the same
+reason, initial values cannot be functions, e.g., `composer.let({ foo: params =>
+params }, composition)` is incorrect.
 
-Variables declared with `composer.let` may be accessed and mutated by functions __running__ as part of the following sequence (irrespective of their place of definition). In other words, name resolution is [dynamic](https://en.wikipedia.org/wiki/Name_resolution_(programming_languages)#Static_versus_dynamic). If a variable declaration is nested inside a declaration of a variable with the same name, the innermost declaration masks the earlier declarations.
+Variables declared with `composer.let` may be accessed and mutated by functions
+__running__ as part of the following sequence (irrespective of their place of
+definition). In other words, name resolution is
+[dynamic](https://en.wikipedia.org/wiki/Name_resolution_(programming_languages)#Static_versus_dynamic).
+If a variable declaration is nested inside a declaration of a variable with the
+same name, the innermost declaration masks the earlier declarations.
 
-For example, the following composition invokes composition `composition` repeatedly `n` times.
+For example, the following composition invokes composition `composition`
+repeatedly `n` times.
 ```javascript
 composer.let({ i: n }, composer.while(() => i-- > 0, composition))
 ```
-Variables declared with `composer.let` are not visible to invoked actions. However, they may be passed as parameters to actions as for instance in:
+Variables declared with `composer.let` are not visible to invoked actions.
+However, they may be passed as parameters to actions as for instance in:
 ```javascript
 composer.let({ n: 42 }, () => ({ n }), 'increment', params => { n = params.n })
 ```
 
-In this example, the variable `n` is exposed to the invoked action as a field of the input parameter object. Moreover, the value of the field `n` of the output parameter object is assigned back to variable `n`.
+In this example, the variable `n` is exposed to the invoked action as a field of
+the input parameter object. Moreover, the value of the 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.
+`composer.mask(composition_1, composition_2, ...)` is meant to be used in
+combination with the `let` combinator. It runs a sequence of compositions
+excluding from their scope the variables declared by the innermost enclosing
+`let`. 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:
+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)))
+    return composer.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 }`.
+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.
+While composer variables are dynamically scoped, judicious use of the `mask`
+combinator can prevent incidental name collision.
 
 ## If
 
-`composer.if(condition, consequent, [alternate])` 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.
+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 _alternate_ composition may be omitted. If _condition_ fails, neither branch
+is executed.
 
-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:
+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 })
 ```
-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:
+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.
+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 `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, 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)` 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.
+A failure of _condition_ or _body_ interrupts the execution. The composition
+returns the error object from the failed component.
 
-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.
+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 }`:
+For instance, the following composition invoked on dictionary `{ n: 28 }`
+returns `{ n: 7 }`:
 ```javascript
 composer.while(params => params.n % 2 === 0, params => { params.n /= 2 })
 ```
-For instance, the following composition invoked on dictionary `{ n: 28 }` returns `{ n: 7, value: false }`:
+For instance, the following composition invoked on dictionary `{ n: 28 }`
+returns `{ n: 7, value: false }`:
 ```javascript
 composer.while_nosave(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 })
 ```
 
 ## Dowhile
 
-`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.
+`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_.
+Like `while_nosave`, `dowhile_nosave` does not implicitly preserve the parameter
+object while evaluating _condition_.
 
 ## Repeat
 
-`composer.repeat(count, body)` invokes _body_ _count_ times.
+`composer.repeat(count, composition_1, composition_2, ...)` invokes a sequence
+of compositions _count_ times.
 
 ## Try
 
 `composer.try(body, handler)` runs _body_ with error handler _handler_.
 
-If _body_ returns an error object, _handler_ is invoked with this error object as its input parameter object. Otherwise, _handler_ is not run.
+If _body_ returns an error object, _handler_ is invoked with this error object
+as its input parameter object. Otherwise, _handler_ is not run.
 
 ## Finally
 
 `composer.finally(body, finalizer)` runs _body_ and then _finalizer_.
 
-The _finalizer_ is invoked in sequence after _body_ even if _body_ returns an error object.
+The _finalizer_ is invoked in sequence after _body_ even if _body_ returns an
+error object. The output parameter object of _body_ (error object or not) is the
+input parameter object of _finalizer_.
 
 ## Retry
 
-`composer.retry(count, body)` runs _body_ and retries _body_ up to _count_ times if it fails. The output parameter object for the composition is either the output parameter object of the successful _body_ invocation or the error object produced by the last _body_ invocation.
+`composer.retry(count, composition_1, composition_2, ...)` runs a sequence of
+compositions retrying the sequence up to _count_ times if it fails. The output
+parameter object for the composition is either the output parameter object of
+the successful sequence invocation or the error object produced by the last
+sequence invocation.
 
 ## Retain
 
-`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_.
+`composer.retain(composition_1, composition_2, ...)` runs a sequence of
+compositions 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 the sequence.
 
-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.
+If the sequence fails, the output of the `retain` combinator is only the error
+object (i.e., the input parameter object is not preserved). In contrast, the
+`retain_catch` combinator always outputs `{ params, result }`, even if `result`
+is an error object.
+
+## Merge
+
+`composer.merge(composition_1, composition_2, ...)` runs a sequence of
+compositions on the input parameter object and merge the output parameter object
+of the sequence into the input parameter object. In other words,
+`composer.merge(composition_1, composition_2, ...)` is a shorthand for:
+```
+composer.seq(composer.retain(composition_1, composition_2, ...), ({ params, result }) => Object.assign(params, 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_.
+`composer.async(composition_1, composition_2, ...)` runs a sequence of
+compositions asynchronously. It invokes the sequence but does not wait for it to
+execute. It immediately returns a dictionary that includes a field named
+`activationId` with the activation id for the sequence invocation.
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
new file mode 100644
index 0000000..17e0e19
--- /dev/null
+++ b/docs/COMMANDS.md
@@ -0,0 +1,99 @@
+# Commands
+
+The `compose` command compiles composition code to a portable JSON format. The
+`deploy` command deploys JSON-encoded compositions. These commands are intended
+as a minimal complement to the OpenWhisk CLI. The OpenWhisk CLI already has the
+capability to configure, invoke, and delete compositions since these are just
+OpenWhisk actions but lacks the capability to create composition actions. The
+`compose` and `deploy` commands bridge this gap. They make it possible to deploy
+compositions as part of the development cycle or in shell scripts. They do not
+replace the OpenWhisk CLI however as they do not duplicate existing OpenWhisk
+CLI capabilities.
+
+## Compose
+
+```
+compose
+```
+```
+Usage:
+  compose composition.js [flags]
+Flags:
+  --ast                  only output the ast for the composition
+  -v, --version          output the composer version
+```
+The `compose` command takes a Javascript module that exports a composition
+object (for example [demo.js](../samples/demo.js)) and compiles this object to a
+portable JSON format on the standard output.
+```
+compose demo.js > demo.json
+```
+If the `--ast` option is specified, the `compose` command only outputs a JSON
+representation of the Abstract Syntax Tree for the composition.
+
+# Deploy
+
+```
+deploy
+```
+```
+Usage:
+  deploy composition composition.json [flags]
+Flags:
+  -a, --annotation KEY=VALUE        add KEY annotation with VALUE
+  -A, --annotation-file KEY=FILE    add KEY annotation with FILE content
+  --apihost HOST                    API HOST
+  -i, --insecure                    bypass certificate checking
+  -u, --auth KEY                    authorization KEY
+  -v, --version                     output the composer version
+  -w, --overwrite                   overwrite actions if already defined
+```
+The `deploy` command deploys a JSON-encoded composition with the given name.
+```
+deploy demo demo.json -w
+```
+```
+ok: created /_/authenticate,/_/success,/_/failure,/_/demo
+```
+
+The `deploy` command synthesizes and deploys a conductor action that implements
+the composition with the given name. It also deploys the composed actions for
+which definitions are provided as part of the composition.
+
+The `deploy` command outputs the list of deployed actions or an error result. If
+an error occurs during deployment, the state of the various actions is unknown.
+
+The `-w` option authorizes the `deploy` command to overwrite existing
+definitions. More precisely, it deletes the deployed actions before recreating
+them. As a result, default parameters, limits, and annotations on preexisting
+actions are lost.
+
+### Annotations
+
+The `deploy` command implicitly annotates the deployed composition action with
+the required `conductor` annotations. Other annotations may be specified by
+means of the flags:
+```
+  -a, --annotation KEY=VALUE        add KEY annotation with VALUE
+  -A, --annotation-file KEY=FILE    add KEY annotation with FILE content
+```
+
+### OpenWhisk instance
+
+Like the OpenWhisk CLI, the `deploy` command supports the following flags for
+specifying the OpenWhisk instance to use:
+```
+  --apihost HOST                    API HOST
+  -i, --insecure                    bypass certificate checking
+  -u, --auth KEY                    authorization KEY
+```
+If the `--apihost` flag is absent, the environment variable `__OW_API_HOST` is
+used in its place. If neither is available, the `deploy` command extracts the
+`APIHOST` key from the whisk property file for the current user.
+
+If the `--auth` flag is absent, the environment variable `__OW_API_KEY` is used
+in its place. If neither is available, the `deploy` command extracts the `AUTH`
+key from the whisk property file for the current user.
+
+The default path for the whisk property file is `$HOME/.wskprops`. It can be
+altered by setting the `WSK_CONFIG_FILE` environment variable.
diff --git a/docs/COMPOSE.md b/docs/COMPOSE.md
deleted file mode 100644
index 5513735..0000000
--- a/docs/COMPOSE.md
+++ /dev/null
@@ -1,260 +0,0 @@
-# Compose Command
-
-The `compose` command makes it possible to deploy compositions from the command line.
-
-The `compose` command is intended as a minimal complement to the OpenWhisk CLI. The OpenWhisk CLI already has the capability to configure, invoke, and delete compositions (since these are just OpenWhisk actions) but lacks the capability to create composition actions. The `compose` command bridges this gap. It makes it possible to deploy compositions as part of the development cycle or in shell scripts. It is not a replacement for the OpenWhisk CLI however as it does not duplicate existing OpenWhisk CLI capabilities. Moreover, for a much richer developer experience, we recommend using [Shell](https://github.com/ibm-functions/shell).
-
-## Usage
-
-```
-compose
-```
-```
-Usage:
-  compose composition.js[on] command [flags]
-Commands:
-  --json                 output the json representation for the composition (default command)
-  --deploy NAME          deploy the composition with name NAME
-  --entity NAME          output the conductor action definition for the composition (giving name NAME to the composition)
-  --entities NAME        convert the composition into an array of action definition (giving name NAME to the composition)
-  --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
-  -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).
-
-The `compose` command has several modes of operation:
-- By default or when the `--json` option is specified, the command returns the composition encoded as a JSON dictionary.
-- When the `--deploy` option is specified, the command deploys the composition given the desired name for the composition.
-- When the `--encode` option is specified, the command returns the Javascript code for the [conductor action](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md) for the composition.
-- When the `--entity` option is specified, the command returns the complete conductor action definition as a JSON dictionary.
-- When the `--entities` option is specified, the command returns an array of action definitions including not only the conductor action for the composition, but possibly also the nested action definitions.
-
-## JSON option
-
-By default, the `compose` command evaluates the composition code and outputs the resulting JSON dictionary:
-```
-compose demo.js
-```
-```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' } }"
-            }
-        }
-    }
-}
-
-## Entity option
-
-With the `--entity` option the `compose` command returns the conductor action definition for the composition.
-```
-compose demo.js --entity demo
-```
-```json
-{
-    "name": "/_/demo",
-    "action": {
-        "exec": {
-            "kind": "nodejs:default",
-            "code": "..."
-        },
-        "annotations": [
-            {
-                "key": "conductor",
-                "value": {
-                    "type": "if",
-                    "test": {
-                        "type": "action",
-                        "name": "/_/authenticate"
-                    },
-                    "consequent": {
-                        "type": "action",
-                        "name": "/_/success"
-                    },
-                    "alternate": {
-                        "type": "action",
-                        "name": "/_/failure"
-                    }
-                }
-            },
-            {
-                "key": "composer",
-                "value": "0.4.0"
-            }
-        ]
-    }
-}
-```
-
-
-## Entities option
-
-With the `--entities` option the `compose` command returns not only the conductor action definition for the composition but also the definitions of nested actions and compositions.
-```
-compose demo.js --entities demo
-```
-```json
-[
-    {
-        "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' } }"
-            }
-        }
-    },
-    {
-        "name": "/_/demo",
-        "action": {
-            "exec": {
-                "kind": "nodejs:default",
-                "code": "..."
-            },
-            "annotations": [
-                {
-                    "key": "conductor",
-                    "value": {
-                        "type": "if",
-                        "test": {
-                            "type": "action",
-                            "name": "/_/authenticate"
-                        },
-                        "consequent": {
-                            "type": "action",
-                            "name": "/_/success"
-                        },
-                        "alternate": {
-                            "type": "action",
-                            "name": "/_/failure"
-                        }
-                    }
-                },
-                {
-                    "key": "composer",
-                    "value": "0.4.0"
-                }
-            ]
-        }
-    }
-]
-
-```
-
-## Deploy option
-
-The `--deploy` option makes it possible to deploy a composition (Javascript or JSON) given the desired name for the composition:
-```
-compose demo.js --deploy demo
-```
-```
-ok: created /_/authenticate,/_/success,/_/failure,/_/demo
-```
-Or:
-```
-compose demo.js > demo.json
-compose demo.json --deploy demo
-```
-```
-ok: created /_/authenticate,/_/success,/_/failure,/_/demo
-```
-The `compose` command synthesizes and deploys a conductor action that implements the
-composition with the given name. It also deploys the composed actions for which
-definitions are provided as part of the composition.
-
-The `compose` command outputs the list of deployed actions or an error result. If an error occurs during deployment, the state of the various actions is unknown.
-
-The `compose` command deletes the deployed actions before recreating them if necessary. As a result, default parameters, limits, and annotations on preexisting actions are lost.
-
-### Configuration
-
-Like the OpenWhisk CLI, the `compose` command supports the following flags for specifying the OpenWhisk deployment to use:
-```
- --apihost HOST         API HOST
-  -u, --auth KEY        authorization KEY
-  -i, --insecure        bypass certificate checking
-```
-If the `--apihost` flag is absent, the environment variable `__OW_API_HOST` is used in its place. If neither is available, the `compose` command extracts the `APIHOST` key from the whisk property file for the current user.
-
-If the `--auth` flag is absent, the environment variable `__OW_API_KEY` is used in its place. If neither is available, the `compose` command extracts the `AUTH` key from the whisk property file for the current user.
-
-The default path for the whisk property file is `$HOME/.wskprops`. It can be altered by setting the `WSK_CONFIG_FILE` environment variable.
-
-## Encode option
-
-The `compose` command returns the code of the conductor action for the composition (Javascript or JSON) when invoked with the `--encode` option.
-For instance, the conductor action code for the [demo.js](../samples/demo.js) composition is [demo-conductor.js](../samples/demo-conductor.js):
-```
-compose demo.js --encode > demo-conductor.js
-```
-This code may be deployed using the OpenWhisk CLI:
-```
-wsk action create demo demo-conductor.js -a conductor true
-```
-```
-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. 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
deleted file mode 100644
index 68bf8eb..0000000
--- a/docs/COMPOSER.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# Composer Module
-
-The [`composer`](../composer.js) Node.js module makes it possible define, deploy, and invoke compositions.
-
-## Installation
-
-To install the `composer` module, use the Node Package Manager:
-```
-npm install @ibm-functions/composer
-```
-To take advantage of the `compose` command, it may be useful to install the module globally as well (`-g` option).
-
-## Example
-
-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')
-
-// define the composition
-const composition = 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' } } }))
-
-// instantiate OpenWhisk client
-const wsk = composer.util.openwhisk({ ignore_certs: true })
-
-wsk.compositions.deploy({ name: '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
-{
-    "message": "success"
-}
-```
-Alternatively, the `compose` command can deploy compositions and the OpenWhisk CLI can invoke compositions. See [COMPOSE.md](COMPOSE.md) for details.
-
-# 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 via the `composer.util` object.
-
-| Helper method  | Example |
-| --:| --- | --- |
-| [`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()` |
-
-## Version
-
-`composer.util.version` returns the version number for the composer module.
-
-## Deserialize
-
-`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.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.util.lower(composition, ['retry'])` will preserve any instance of the `retry` combinator but replace other non-primitive combinators sur as `retain`.
-
-## Encode
-
-`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 optional lowering. See [lower](#lower) for details.
-
-## Openwhisk client
-
-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.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
-
-`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 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
-
-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')
-```
-Updating or deleting a conductor action only affect the action itself. It does not affect any other action deployed as part of the composition.
diff --git a/docs/COMPOSITIONS.md b/docs/COMPOSITIONS.md
index 87f51a0..b06b331 100644
--- a/docs/COMPOSITIONS.md
+++ b/docs/COMPOSITIONS.md
@@ -1,10 +1,14 @@
 # Compositions
  
-Composer makes it possible to assemble actions into rich workflows called _compositions_. An example composition is described in [../README.md](../README.md).
+Composer makes it possible to assemble actions into rich workflows called
+_compositions_. An example composition is described in
+[../README.md](../README.md).
 
 ## Control flow
 
-Compositions can express the control flow of typical a sequential imperative programming language: sequences, conditionals, loops, error handling. This control flow is specified using _combinator_ methods such as:
+Compositions can express the control flow of typical a sequential imperative
+programming language: sequences, conditionals, loops, structured error handling.
+This control flow is specified using _combinator_ methods such as:
 - `composer.sequence(firstAction, secondAction)`
 - `composer.if(conditionAction, consequentAction, alternateAction)`
 - `composer.try(bodyAction, handlerAction)`
@@ -13,29 +17,54 @@
 
 ## Composition objects
 
-Combinators return composition objects, i.e., instances of the `Composition` class.
+Combinators return composition objects, i.e., instances of the `Composition`
+class.
 
 ## Parameter objects and error objects
 
-A composition, like any action, accepts a JSON dictionary (the _input parameter object_) and produces a JSON dictionary (the _output parameter object_). An output parameter object with an `error` field is an _error object_. A composition _fails_ if it produces an error object.
+A composition, like any action, accepts a JSON dictionary (the _input parameter
+object_) and produces a JSON dictionary (the _output parameter object_). An
+output parameter object with an `error` field is an _error object_. A
+composition _fails_ if it produces an error object.
 
-By convention, an error object returned by a composition is stripped from all fields except from the `error` field. This behavior is consistent with the OpenWhisk action semantics, e.g., the action with code `function main() { return { error: 'KO', message: 'OK' } }` outputs `{ error: 'KO' }`.
+By convention, an error object returned by a composition is stripped from all
+fields except from the `error` field. This behavior is consistent with the
+OpenWhisk action semantics, e.g., the action with code `function main() { return
+{ error: 'KO', message: 'OK' } }` outputs `{ error: 'KO' }`.
 
-Error objects play a specific role as they interrupt the normal flow of execution, akin to exceptions in traditional programming languages. For instance, if a component of a sequence returns an error object, the remainder of the sequence is not executed. Moreover, if the sequence is enclosed in an error handling composition like a `composer.try(sequence, handler)` combinator, the execution continues with the error handler.
+Error objects play a specific role as they interrupt the normal flow of
+execution, akin to exceptions in traditional programming languages. For
+instance, if a component of a sequence returns an error object, the remainder of
+the sequence is not executed. Moreover, if the sequence is enclosed in an error
+handling composition like a `composer.try(sequence, handler)` combinator, the
+execution continues with the error handler.
 
 ## Data flow
 
-The invocation of a composition triggers a series of computations (possibly empty, e.g., for the empty sequence) obtained by chaining the components of the composition along the path of execution. The input parameter object for the composition is the input parameter object of the first component in the chain. The output parameter object of a component in the chain is typically the input parameter object for the next component if any or the output parameter object for the composition if this is the final component in the chain.
+The invocation of a composition triggers a series of computations (possibly
+empty, e.g., for the empty sequence) obtained by chaining the components of the
+composition along the path of execution. The input parameter object for the
+composition is the input parameter object of the first component in the chain.
+The output parameter object of a component in the chain is typically the input
+parameter object for the next component if any or the output parameter object
+for the composition if this is the final component in the chain.
 
-For example, the composition `composer.sequence('triple', 'increment')` invokes the `increment` action on the output of the `triple` action.
+For example, the composition `composer.sequence('triple', 'increment')` invokes
+the `increment` action on the output of the `triple` action.
 
-Some combinators however are designed to alter the default flow of data. For instance, the `composer.retain(myAction)` composition returns a combination of the input parameter object and the output parameter object of `myAction`.
+Some combinators however are designed to alter the default flow of data. For
+instance, the `composer.merge('myAction')` composition merges the input and
+output parameter objects of `myAction`.
 
 ## Components
 
-Components of a compositions can be actions, Javascript functions, or compositions.
+Components of a compositions can be actions, Javascript functions, or
+compositions.
 
-Javascript functions can be viewed as simple, anonymous actions that do not need to be deployed and managed separately from the composition they belong to. Functions are typically used to alter a parameter object between two actions that expect different schemas, as in:
+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.sequence('getUserNameAndPassword', params => ({ key = btoa(params.user + ':' + params.password) }), 'authenticate')
 ```
@@ -43,15 +72,21 @@
 ```javascript
 composer.if('isEven', 'half', composer.sequence('triple', 'increment'))
 ```
-Compositions can reference other compositions by name. For instance, assuming we deploy the sequential composition of the `triple` and `increment` actions as the composition `tripleAndIncrement`, the following code behaves identically to the previous example:
+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')
 ```
-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.
+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
+## Embedded action definitions
 
-A composition can embed the definitions of none, some, or all the composed actions as illustrated in [demo.js](../samples/demo.js):
+A composition can embed the definitions of none, some, or all the composed
+actions as illustrated in [demo.js](../samples/demo.js):
 ```javascript
 composer.if(
     composer.action('authenticate', { action: function ({ password }) { return { value: password === 'abc123' } } }),
@@ -61,11 +96,13 @@
 ```
 Deploying such a composition deploys the embedded actions.
 
-## 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 embedded inside the composition.
-
 ## 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.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
+Compositions are implemented by means of OpenWhisk [conductor
+actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md).
+Compositions have all the attributes and capabilities of an action, e.g.,
+default parameters, limits, blocking invocation, web export. Execution
+[traces](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md#activations)
+and
+[limits](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md#limits)
+of compositions follow from conductor actions.
diff --git a/docs/FORMAT.md b/docs/FORMAT.md
deleted file mode 100644
index 47f0901..0000000
--- a/docs/FORMAT.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# JSON Format
-
-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:object |
-| `literal` or `value` | value:json value |
-| `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 |
-| `async` | body:composition |
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 7b7b26c..0cf3e8b 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,14 +1,11 @@
 # Composer Package
 
 The Composer package consists of:
-* the [composer](../composer.js) Node.js module for authoring, deploying, and invoking compositions,
-* the [compose](../bin/compose) command for managing compositions from the command line.
+* the [composer](../composer.js) Node.js module for authoring compositions,
+* the [compose](../bin/compose.js) and [deploy](../bin/deploy.js) commands for
+  managing compositions from the command line.
 
 The documentation for the Composer package is organized as follows:
 - [COMPOSITIONS.md](COMPOSITIONS.md) gives a brief introduction to compositions.
-- [COMPOSER.md](COMPOSER.md) documents the `composer` module.
-- [COMPOSE.md](COMPOSE.md) documents the `compose` command.
-- [COMBINATORS.md](COMBINATORS.md) documents the methods of the `composer` object.
-- [FORMAT.md](FORMAT.md) documents the JSON format for encoding compositions.
-- [TEMPLATES.md](TEMPLATES.md) demonstrates various composition templates.
-- The [tutorials](tutorials) folder includes various tutorials.
+- [COMBINATORS.md](COMBINATORS.md) explains the composition constructs.
+- [COMMANDS.md](COMMANDS.md) describes the `compose` and `deploy` commands.
diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md
deleted file mode 100644
index 0d61b56..0000000
--- a/docs/TEMPLATES.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# Composition Templates
-
-Composer makes it easy to define new composition templates. In essence, a composition template is just a composition with one or more variable arguments. In this document, we discuss key concepts of composition templates and describe a few useful templates. To use any of these templates, simply paste the template code into your composition file.
-
-## A first example
-
-The following composition templates invokes composition `composition` and returns the input dictionary augmented with the fields of the output dictionary, overwriting existing fields on name clashes:
-```javascript
-function merge(composition) {
-    return composer.seq(composer.retain(composition), ({ params, result }) => Object.assign(params, result))
-}
-```
-
-To instantiate a composition template, simply apply the Javascript function to concrete arguments as in:
-```javascript
-merge(({ n }) => ({ nPlusOne: n + 1 }))
-```
-For example, invoking this composition on input `{ n: 42 }` outputs `{ n: 42, nPlusOne: 43 }`.
-
-The `composer` object itself may be extended with the new template using _monkey patching_:
-```javascript
-composer.merge = composition => composition.seq(composer.retain(composition), ({ params, result }) => Object.assign(params, result))
-```
-
-## On the importance of mask
-
-Many predefined combinators are actually templates over more primitive combinators. For instance the `retain` combinator is essentially defined as follows:
-```javascript
-function retain(composition) {
-    return composer.let(
-        { params: null },
-        args => { params = args },
-        composer.mask(composition),
-        result => ({ params, result }))
-}
-```
-
-This implementation first declares a variable named `params` using `composer.let`. It then saves the input dictionary by assigning it to `params`. Next it runs `composition`. It produces the final result by combining the output dictionary (bound to `result`) with the input dictionary (bound to `params`).
-
-It is important to notice the use of the `mask` combinator here. Since this implementation introduces a variable named `params` and invokes the parameter `composition` in the scope of the `params` variable, this variable declaration may clash with another declaration of `params` in the user code. By wrapping the `composition` invocation with `mask` we ensure that the `params` variable declared in this template is hidden from `composition`. Thanks to `mask`, the following composition correctly writes ```'Hi there!'``` to the standard output:
-```javascript
-composer.let({ params: 'Hi there!' }, retain(() => { console.log(params) }))
-```
-
-Because `composer` variables are dynamically scoped, without the `mask` combinator, the action log would show the value of the `params` variable declared in the template instead of the expected value.
-
-## Pseudo parallel
-
-The `composer` module does not support parallel execution at this time but we can fake it as follows:
-```javascript
-composer.par = (f, g) =>
-    composer.let(
-        { input: null, left: null },
-        args => { input = args },
-        composer.mask(f),
-        args => { left = args; return input },
-        composer.mask(g),
-        right => ({ left, right }))
-```
-This code pretends to execute compositions `f` and `g` in parallel. They both receive the same input dictionary. The output dictionary for the composition has two fields: `left` carries the result of `f` and `right` carries the result of `g`. In fact the two composition are executed in sequence but `let`, `mask`, and a few Javascript function can flow the data as if running in parallel.
-
-This implementation does not handle exceptions in `f` or `g`. This is left as an exercise to the reader.
-
-## Apply
-
-This `apply` combinator makes it possible to invoke a `composition` on a `field` of the input dictionary, leaving other fields unchanged.
-```javascript
-// example.js
-composer.apply = (field, composition) =>
-    composer.let(
-        { field },
-        composer.retain(p => p[field], composer.mask(composition)),
-        p => { p.params[field] = p.result; return p.params })
-
-composer.apply('payload', p => { p.n++ })
-```
-
-```
-compose example.js --deploy example
-ok: created /_/example
-```
-```
-wsk action invoke example -r -p payload '{"n":1,"p":42}'
-{
-    "payload": {
-        "n": 2,
-        "p": 42
-    }
-}
-```
-In this example, the `let` combinator is used to capture the desired field name: it binds the `field` variable to the field name.
-
-## Forward
-
-This `forward` combinator excludes the specified `fields` from the input dictionary for `composition` and restores them afterwards. It is useful for instance to hide secrets from a composition.
-
-```javascript
-composer.forward = (fields, composition) =>
-    composer.let(
-        { fields },
-        composer.retain(p => require('lodash').omit(p, ...fields), composer.mask(composition)),
-        ({ params, result }) => Object.assign(result, require('lodash').pick(params, ...fields)))
-
-composer.forward(['user', 'password'], untrustedComposition)
-```
-
-## Inject and extract
-
-The following combinators make it possible to bind a field of the parameter object to the value of the homonymous variable and vice versa.
-```javascript
-composer.inject = v => composer.seq(composer.let({ v }, params => { params[v] = eval(v) }))
-composer.extract = v => composer.seq(composer.let({ v }, params => { eval(`${v} = params[v]`); delete params[v] }))
-
-composer.let({ token: null }, 'getSecretToken', composer.extract('token'), untrustedAction, composer.inject('token'), trustedAction)
-```
diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md
deleted file mode 100644
index 1070fc3..0000000
--- a/docs/tutorials/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Tutorials
-
-This folder contains a few tutorials to get started:
-* [Introduction to Serverless
-  Composition](introduction/README.md): Setting up your
-  programming environment and getting started with Shell and Composer.
-* [Building a Translation Slack Bot with Serverless
-  Composition](translateBot/README.md): A more advanced tutorial
-  using Composition to build a serverless Slack chatbot that does language
-  translation.
diff --git a/docs/tutorials/introduction/README.md b/docs/tutorials/introduction/README.md
deleted file mode 100644
index d4aa87c..0000000
--- a/docs/tutorials/introduction/README.md
+++ /dev/null
@@ -1,468 +0,0 @@
-# Introduction to Serverless Composition
-
-Composer is an [IBM Cloud Functions](https://ibm.biz/openwhisk)
-programming model for composing individual functions into larger
-applications. Compositions, informally named _apps_, run in the cloud
-using automatically managed compute and memory resources. Composer is
-an extension of the function-as-a-service computing model, and enables
-stateful computation, control flow, and rich patterns of data flow.
-
-Programming for the serverless cloud is a uniquely new experience. For
-this reason, we have developed a unified environment that offers the
-benefits and familiarity of a command line interface, with
-visualization and a graphical interface to assist in certain
-tasks. This environment is offered through a new tool called 
-[IBM Cloud Shell](https://github.com/ibm-functions/shell), or just _Shell_.
-
-## Shell quick start
-
-Shell is a cross-platform desktop application powered by [Electron](https://electronjs.org/). 
-
-### Before you run your first app
-
-You must have a valid IBM Cloud (i.e., Bluemix)
-[account](https://ibm.biz/openwhisk), or deploy [Apache
-OpenWhisk](https://github.com/apache/incubator-openwhisk)
-locally. This is needed because Composer builds on and extends Apache
-OpenWhisk, which powers IBM Cloud Functions.
-
-* Existing `wsk` CLI users: You can go directly to [Installing Shell](#installing-shell). 
-
-* _New users using composer with IBM Cloud Functions:_ you need an IBM Cloud
-[account](https://ibm.biz/openwhisk), and the [IBM Cloud CLI](https://console.bluemix.net/docs/cli/reference/bluemix_cli/download_cli.html#download_install) (`bx`). You will also need to install the Cloud Function Plugin for bx: 
-
-  ``` 
-  $ bx plugin install Cloud-Functions -r bluemix
-  ```
-
-  After installing `bx` and the Cloud Function plugin, use `bx login` to generate a access token for Cloud Function. 
-
-  ``` 
-  $ bx login -a api.ng.bluemix.net -o yourBluemixOrg -s yourBluemixSpace
-  ```
-
-  Run a test to generate credentials and verify your setup. Here, we perform a blocking (synchronous) invocation of echo, passing it "hello" as an argument. If you see the return message, you are good to go.  
-  ```
-  $ bx wsk action invoke /whisk.system/utils/echo -p message hello --result
-  {
-    "message": "hello"      
-  }
-  ```
-
-* _New users using composer with Apache OpenWhisk:_  you need a valid `$HOME/.wskprops` file and a locally deployed OpenWhisk instance.
-
-
-### Installing Shell
-
-Shell is currently distributed through the [Node
-package manager](https://www.npmjs.com/package/@ibm-functions/shell).
-
-```
-$ npm install -g @ibm-functions/shell
-```
-
-We roll out frequent updates and bug fixes. You can check for new
-releases via `fsh version -u`.
-
-```
-$ fsh version -u
-You are currently on version x.y.z
-Checking for updates... you are up to date!
-```
-
-We recommend updating the shell via the same `npm install` command
-shown earlier.  Consult the [troubleshooting
-guide](https://github.com/ibm-functions/shell/blob/master/npm.md) if
-your installation fails.
-
-
-### Starting Shell 
-
-```
-$ fsh shell
-```
-
-You will see a window popping up. Welcome to Shell!
-
-_Tip:_ If you are using Mac, you can keep Shell in the dock by right-clicking on the blue Cloud Function Shell icon and choose `Options > Keep in Dock`. Next time you can click on the icon in the dock to launch Shell. 
-
-
-## Your first app
-
-Compositions are described using a [Node.js library](../../README.md)
-which offers an SDK for describing control structures. We call these
-_combinators_.  The simplest combinator constructs a sequence. For example, here is
-a snippet for your first app: 
-
-```javascript
-composer.sequence(args => ({msg: `hello ${args.name}!`}))
-```
-
-The code describes a sequence app with just one function that is inlined for convenience. 
-
-There are two ways to deploy a composition code snippet. One is using Shell to write the code and deploy it. The other is using any editor to write this code, save it as a local file, and deploy the file using a Shell command. We describe both here.  
-
-
-### Write an app in Shell
-
-In Shell, enter 
-
-```bash
-# enter in Shell
-> compose myApp 
-```
-
-where `myApp` is the name of the app in the cloud. This command opens a built-in editor in a sidecar for writing code. Copy the `composer.sequence` code above and paste it into the editor. Hit "Deploy" at the bottom stripe to deploy it. After the app is successfully deployed, Shell will show a flow graph that represents the textual composition code at the bottom of the editor as a verification. 
-
-_Tip:_ Enter `edit myApp` to edit `myApp` after it is deployed. 
-
-|<img src="editor.png" width="50%" title="first app in shell">|
-|:--:|
-|Your first app, composed in Shell.|
-
-
-### Write an app in an external editor
-
-You may also use your favorite editor to compose apps. When finished, save your code to a file on your machine with the extension `.js`. To view your local composition javascript file as a graph, enter
-
-```bash
-# enter in Shell
-> app preview path/to/file.js
-```
-
-Shell watches the file you are editing and automatically
-updates the graph as you compose. You can use this active preview mode
-to incrementally build your application, sanity checking your control
-flow as you go. 
-
-|<img src="preview.png" width="50%" title="preview app in shell">|
-|:--:|
-|Writing an app in my own editor, and previewing the code as a graph in Shell.|
-
-
-To deploy a local composition file to the cloud, enter this command:
-```bash
-# enter in Shell
-> app create myApp path/to/file.js
-```
-
-Again, `myApp` is the name of the app in the cloud. 
-
-_Tip_: If you have an action already named `myApp`, the shell will report a name conflict. Use a different name for your app, or use `app update` if you want to update an existing app. Apps are stored as OpenWhisk actions, and hence the naming restrictions for OpenWhisk apply.
-
-
-## Running your first app
-
-Run your first app using this command: 
- 
-```bash
-# enter in Shell
-> app invoke myApp -p name composer
-```
-
-You will see the result in the sidecar. Click on different buttons in the sidecar bottom stripe to explore different views. 
-
-_Tip #1:_ Enter `app invoke --help` to view the usage of the `app invoke` command. You can access the usage information of other commands in Shell in the same way using `--help`. 
-
-_Tip #2:_ Enter `session list` to view a list of previous app executions. 
-
-
-## Composing OpenWhisk actions
-
-Combinators accept either inline Node.js functions or actions by name.
-For the latter, you may use a fully qualified name of an action (i.e.,
-`/namespace[/package]/action`) or its short name. Here is an example
-using the `date` action from the `/whisk.system/utils` package.
-
-```javascript
-composer.sequence('/whisk.system/utils/date')
-```
-
-A composition which refers to actions by name will not run correctly
-if there are missing referenced entities. The `app preview` will
-highlight any missing entities. As an example, preview the built-in
-[`@demos/if.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/if.js)
-composition, which is [described in the next
-section](#if-combinator). The control flow graph should be
-self-explanatory. An action is gray when it is not yet deployed, and
-blue otherwise.
-
-```bash
-# enter in Shell
-> app preview @demos/if.js  
-```
-
-|<img src="if-preview.png" title="if combinator preview" width="50%">|
-|:--:|
-|Control flow graph for `if` combinator. An action that is not yet deployed is gray, and blue otherwise.|
-
-_Tip:_ Shell supports `wsk` CLI commands for deploying OpenWhisk actions. We will explain how to do so next. You can also read more about using `wsk` commands in Shell [here](https://github.com/ibm-functions/shell/blob/master/fsh.md). 
-
-
-### Composing inline functions vs. OpenWhisk actions
-
-The main difference between using an inline function verses a OpenWhisk action in a composition is that an inline function does not generate an activation like an OpenWhisk action. [Activations](https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md) record runtime data like the execution time and output. They are useful for debugging.
-
-When making a real app, we encourage you to create the main components as OpenWhisk actions, as OpenWhisk actions can be reused by different apps and are easier to debug. Inline functions can be used as a convenient way to connect different components together (such as renaming input and output, generating an error message) and are better kept short and simple. 
-
-
-## Compositions by example
-
-You now have the basic tools to build a serverless composition, invoke
-it, and inspect its execution and result. Currently, Composer offers [13 different combinators](../../README.md#combinators) to support conditions, iterations, error handling, variable declarations and other common programming constructs for building various types of apps. 
-
-This section will introduce you to some combinators for creating richer control and data flow, while other combinators are covered in the [reference manual](../../README.md). All javascript code described below is [bundled in Shell](https://github.com/ibm-functions/shell/blob/master/app/demos) and can be accessed within Shell using the prefix `@demos/`. 
-
-
-### `if` combinator
-
-An `if` combinator allows you to describe a conditional flow with a
-`then` and optional `else` branch. This is convenient for
-short-circuiting a sequence for example, or taking data-dependent
-paths in the control flow.
-
-Here is a short example. Say you have a function `welcome` which generates an HTML page.
-
-```javascript
-// @demos/welcome.js
-let welcome = args => ({ html: `<html><body>welcome ${args.name}!</body></html>` })
-```
-
-In order to use this function as part of an authenticated API, we can
-modify the function itself to introduce authentication middleware. Or,
-we can compose it with an authentication function.
-
-```javascript
-// @demos/authenticate.js
-let authenticate = args => ({ value: args.token === "secret" })
-```
-
-For illustration purposes, `authenticate` is a simple token based
-checker. If the token equals the secret value, return `true`, and
-`false` otherwise. In a real scenario, this function may delegate to a
-third party service or identity provider.
-
-Let's add a third function, this one to deal with the
-non-authenticated case and return a different HTML page, perhaps
-informing the client to try again with the proper secret.
-
-```javascript
-// @demos/login.js
-let login = args => ({ html: `<html><body>please say the magic word.</body></html>` })
-```
-
-The `if` combinator composes these three functions as you might
-expect.
-
-```javascript
-// @demos/if.js
-composer.if(
-  /* cond */ 'authenticate',
-  /* then */ 'welcome',
-  /* else */ 'login')
-```
-
-Now, enter the following in Shell to deploy and run the app.
-
-```bash
-# enter in Shell
-# create required actions
-> action create authenticate @demos/authenticate.js
-> action create welcome @demos/welcome.js
-> action create login @demos/login.js
-
-# create app
-> app create if @demos/if.js
-
-# invoke app, with no secret parameter
-> app invoke if
-{
-  html: "<html><body>please say the magic word.</body></html>"
-}
-
-# now invoke with secret parameter
-> app invoke if -p token secret -p name if-combinator
-{
-  html: "<html><body>welcome if-combinator!</body></html>"
-}
-```
-
-_Tip:_ You can see the output data of an action node in the `Session Flow` graph by clicking on the node. This will bring you to the corresponding activation. _Note:_ An inline function node is not clickable as it does not generate an activation. 
-
-Each of the activations will have a different session id, which are reported by listing the available sessions.
-
-```bash
-# enter in Shell
-> session list 
-```
-
-Clicking on a session id from the list will open that session in the sidecar. 
-
-_Tip:_ Clicking on a session id invokes the command `session get sessionId` to view the session info in the sidecar. 
-
-
-### `try` combinator
-
-Another common composition pattern is for error handling and
-recovery. Composer offers a `try` combinator that is analogous to
-`try-catch`.
-
-A example to illustrate using `try` is a schema or data validation
-action. Let `validate` be an action which checks if a string is base64
-encoded, and which throws an exception if the input is not valid. A
-`try` combinator allows an error handler to rewrite the result, as
-one example, to suite the particular usage scenario in the app.
-
-```javascript
-// @demos/try.js
-composer.try(
-  /* try   */ 'validate',
-  /* catch */ args => ({ ok: false }))
-```
-
-The `validate` action is available as [`@demos/validate.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/validate.js) and the
-composition as [`@demos/try.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/try.js) for your convenience.
-
-```bash
-# enter in Shell
-# create validate action
-> action create validate @demos/validate.js
-
-# create app
-> app create try @demos/try.js
-
-# invoke app with valid parameter
-> app invoke try -p str aGVsbG8gdHJ5IQ==
-{
-  ok: true
-}
-
-# and now for the failing case
-> app invoke try -p str bogus
-{
-  ok: false
-}
-```
-
-It is worth looking at the session flow of the second app invoke where
-the catch handler is invoked.
-
-```bash
-# enter in Shell
-> session get --last try
-```
-
-|<img src="try-session.png" title="try session with exception" width="50%">|
-|:--:|
-|Session execution for `try` where the handler is invoked.|
-
-
-Notice that the `validate` action failed, as expected. This is
-visually recognized by the red-colored action, and the hover text which
-shows the action result containing the error. The app result is
-successful however, as the handler rewrites the exception into a
-different result.
-
-## Nesting and forwarding
-
-An important property of the combinators is that they nest. This
-encourages modularity and composition reuse. The example that follows
-illustrates both composition nesting, and data forwarding. The example
-builds on the `try` app described in the previous section. Here, after
-the validate task, we extend the composition with a base64 decoder to
-render the input `str` in plain text.
-
-Recall that the result of the `validate` task is `{ok: true}`,
-not the `str` argument that it processed. So we need a way to forward
-`str` around this action. In other words, we _retain_ the input
-arguments to `validate`, and pass them to the next action in the
-sequence.  Composer offers a combinator for just this purpose. Below
-is the composition showing the inner sequence with the data forwarding
-combinator `retain`.
-
-```javascript
-// @demos/retain.js
-composer.try(
-  composer.sequence(
-    composer.retain('validate'),
-    args => ({ text: new Buffer(args.params.str, 'base64').toString() })),
-  args => ({ ok: false }))
-```
-
-The `retain` combinator produces an output with two fields: `params`
-and `result`. The former is the input parameter of the
-composition. The latter is the output of `validate`. The control and
-dataflow for this composition is shown below, and is available in the
-shell as
-[`@demos/retain.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/retain.js).
-
-```bash
-# enter in Shell
-> app preview @demos/retain.js
-```
-
-|<img src="retain.png" title="retain combinator and nesting" width="50%">|
-|:--:|
-|Control flow graph showing the `retain` combinator and the implied dataflow around `validate`.|
-
-The app will now produce the decoded text as its final output.
-
-```bash
-# enter in Shell
-# create app
-> app create retain @demos/retain.js
-
-# invoke app with valid parameter
-> app invoke retain -p str aGVsbG8gdHJ5IQ==
-{
-  text: "hello try!"
-}
-
-# and now for the failing case
-> app invoke retain -p str bogus
-{
-  ok: false
-}
-```
-
-## Variables and scoping
-
-The composer allows you to introduce variables within a composition,
-and to limit their scope. This is useful when you have to introduce
-service keys and credentials for example. A scoped variable is defined
-using `let`. The example below illustrates how you might introduce a
-"secret" for a specific task without its value escaping to other
-compositions or functions.
-
-```javascript
-// @demos/let.js
-composer.sequence(
-  composer.let({secret: 42},
-    composer.task(_ => ({ ok: secret === 42 }))),
-  composer.task(_ => ({ ok: (typeof secret === 'undefined') })))
-```
-
-The composition will execute successfully only if `secret` is not
-leaked to the final task in the composition, while the value is
-available inside the task nested within the `let`.
-
-```bash
-# enter in Shell
-> app create let @demos/let.js
-> app invoke let
-{
-  ok: true
-}
-```
-
-## Other combinators
-
-The examples shown here illustrate the more common combinators you
-may use to create serverless compositions. There are more combinators
-available in the Composer library. Refer to the [Composer reference
-manual](../../README.md) for more details.
-
-## Next step
-
-Try the second tutorial, [building a translation chatbot](../translateBot/README.md). 
-
diff --git a/docs/tutorials/introduction/editor.png b/docs/tutorials/introduction/editor.png
deleted file mode 100644
index 47f446c..0000000
--- a/docs/tutorials/introduction/editor.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/introduction/hello-composition.png b/docs/tutorials/introduction/hello-composition.png
deleted file mode 100644
index 41dde1e..0000000
--- a/docs/tutorials/introduction/hello-composition.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/introduction/hello-session.png b/docs/tutorials/introduction/hello-session.png
deleted file mode 100644
index 5ca5b1b..0000000
--- a/docs/tutorials/introduction/hello-session.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/introduction/if-preview.png b/docs/tutorials/introduction/if-preview.png
deleted file mode 100644
index d1fd1c4..0000000
--- a/docs/tutorials/introduction/if-preview.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/introduction/preview.png b/docs/tutorials/introduction/preview.png
deleted file mode 100644
index b2ddea9..0000000
--- a/docs/tutorials/introduction/preview.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/introduction/retain.png b/docs/tutorials/introduction/retain.png
deleted file mode 100644
index 7266d34..0000000
--- a/docs/tutorials/introduction/retain.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/introduction/try-session.png b/docs/tutorials/introduction/try-session.png
deleted file mode 100644
index 855bf6a..0000000
--- a/docs/tutorials/introduction/try-session.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/translateBot/README.md b/docs/tutorials/translateBot/README.md
deleted file mode 100644
index cd4e41c..0000000
--- a/docs/tutorials/translateBot/README.md
+++ /dev/null
@@ -1,260 +0,0 @@
-# Building a Translation Slack Bot with Serverless Composition 
-
-Let's do something fun with Composition: We will build a serverless chatbot that translates the user's message using the IBM Watson Language Translation services. This tutorial assumes you have finished the [introduction](../introduction/README.md), which describes the basics about Composition and the instructions to setup the development environment ([Shell](https://github.com/ibm-functions/shell)). 
-
-Here's what our chatbot does. 
-* First, it identifies the language of the user's message. 
-* If the language is not English, it translates the message to English. 
-* If the language is already English, just to make things more fun, it translates the message to "Shakespeare English". 
-* If the message is not translatable (such as if the message is a number), it returns a error message. 
-
-Let's get started. 
-
-## Composing the app
-
-Here is our composition code, using the [Composer NodeJs library](../../README.md). 
-```javascript
-composer.try(
-    composer.sequence(
-        'myWatsonTranslator/languageId',
-        composer.if(
-            p => p.language !== 'en',
-            composer.sequence(
-                p => ({translateFrom: p.language, translateTo: 'en', payload: p.payload}),
-                'myWatsonTranslator/translator'
-            ),
-            composer.sequence(
-                p => ({text: p.payload}),
-                'en2shakespeare'
-            )
-        )
-    ),
-    err => ({payload: 'Sorry we cannot translate your text'})
-)
-```
-
-In Shell, enter
-
-```bash
-# in Shell
-> compose myTranslateApp
-```
-
-Copy and paste the javascript code above to the editor, and hit "Deploy". You will see a graph that represents your the textual composition code when the app is successfully deployed. 
-
-|<img src="myTranslateApp-edit.png" width="90%" title="Compose your translate app">|
-|:--:|
-|Your translate app. _Tip:_ You can click on the "full width" icon at the bottom right of the sidecar to expand the sidecar to full width.|
-
-This app first calls an OpenWhisk action `myWatsonTranslator/languageId` to identify the language type. Then it uses the `if` combinator to say if the identified language is not English, call `myWatsonTranslator/translator` to translate the input message (in `payload`) to English. Otherwise, call `en2shakespeare` to translate English to Shakespeare English. This sequence is wrapped in a `try` combinator to catch any error in the process. Short inline functions are used to quickly check a property value, rename data based on the requirement of an action, and generate an error message. 
-
-All action nodes in the graph are gray at this moment because we don't have those actions deployed yet. 
-
-
-## Using IBM Watson Language Translation service 
-We use a built-in OpenWhisk package for using IBM Watson Language Translation service. To do so you need to have a valid [IBM Cloud account](https://www.ibm.com/cloud/) and a Language Translation service instance under your namespace. The service offers a lite plan that is free of charge. Here we will quickly walk through how to get the service credential and set up the Watson Translation OpenWhisk package. 
-
-If you already have the Watson Translation OpenWhisk package setup, change `myWatsonTranslator` in the composition code to the name of your translation package and redeploy the app. If you see `languageId` and `translator` in the graph become blue, you can move directly to the [next section](#creating-en2shakespeare). 
-
-_Tip:_ You can edit an exiting app using the command `edit appName`.
-
-Follow the steps here to get your Language Translation service credential: 
-1. Go to your [IBM Cloud dashboard](https://console.bluemix.net/dashboard/apps)
-2. If you see a `Language Translator` service offering in your list, click on it and go to Step 5. Otherwise, click on the `Create resource` button at the upper right. 
-3. Search for "Language Translation" and click on it. 
-4. Select the free lite plan, then click "Create" at the bottom to create the service instance.
-5. You will be at the Language Translation service homepage. Click on the "Service credentials" tab at the left pane. 
-6. Choose a service credential that you'd like to use form the list. If you don't see any credentials, click "New credential" then "Add" to add a new credential. 
-7. There is a "View credentials" option that you can click to expand. You'll see a `username` and a `password`. We will use them to set up the Watson Translation OpenWhisk package. 
-
-Now go back to Shell and enter the following command to set up the translation package with your credential: 
-
-```bash
-# in Shell
-> wsk package bind /whisk.system/watson-translator myWatsonTranslator -p username MYUSERNAME -p password MYPASSWORD 
-```
-
-Replace `MYUSERNAME` and `MYPASSWORD` with your username and password. This command creates a package under your namespace called `myWatsonTranslator` from the built-in `whisk.system/watson-translator` package, and binds your service credentials to it. You can now try to invoke an action in this package to test if things are set up correctly:
-
-```bash
-# in Shell
-> action invoke myWatsonTranslator/languageId -p payload "bonjour"
-{
-    "language": "fr",
-    "payload": "bonjour",
-    "confidence": 0.799347
-}
-```
-
-_Tip:_ You can read more about using OpenWhisk packages [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/packages.md).
-
-## Creating en2shakespeare
-
-`en2shakespeare` is a typical example of turning a web API service into an OpenWhisk action (cloud function). It uses the `request` npm module to send an http request. Here's the code:
-
-```javascript
-var request = require("request");
-function main(params) {
-    var options = {
-        url: "http://api.funtranslations.com/translate/shakespeare.json",
-        qs: {
-            text: params.text,
-            api_key: params.apiKey
-        },
-        json: true
-    };    
-    return new Promise(function(resolve, reject) {
-        request(options, function(err, resp) {
-            if (err) {
-                reject({error: err})
-            }
-            resolve({
-                payload: resp.body.contents.translated
-            });
-        });
-    });
-};
-```
-
-In Shell, enter
-
-``` bash
-# in Shell
-> new en2shakespeare 
-```
-
-Copy and paste the above javascript code into the editor and hit `Deploy` to deploy the action. 
-
-Now, enter `app get myTranslateApp` in Shell and you should see all the nodes blue. We can now run the app.
-
-_Note:_ `en2shakespeare` uses an API from [Fun Translations](http://funtranslations.com/). The API allows some free calls without providing an API key. You can subscribe this API with Fun Translations and change `params.apiKey` in the code to be your key. 
-
-_Tip #1:_ OpenWhisk Node.js runtime provides [several built-in npm modules](https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md#nodejs-version-6-environment), including `request` that we use here. You can also create a [zip action](https://github.com/apache/incubator-openwhisk/blob/master/docs/actions.md#packaging-an-action-as-a-nodejs-module) that uses any custom modules you'd like.
-
-_Tip #2:_ You can edit an existing action using `edit actionName`
-
-## Running the app
-
-```bash
-# enter in Shell
-> app invoke myTranslateApp -p payload "Mieux vaut prévenir que guérir"
-{    
-    "payload": "Prevention is better than cure"
-}
-> app invoke myTranslateApp -p payload "hello world"
-{
-    "payload": "Valorous morrow to thee,  sir ordinary"
-}
-> app invoke myTranslateApp -p payload "3.14159"
-{
-    "payload": "Sorry we cannot translate your text"
-}
-```
-
-Checkout the `Session Flow` and `Trace` tab of a session to look at the execution path and trace of your app. You can use `session list` to see a list of recent sessions, and click on a session to view its detail in the sidecar. 
-
-## Connecting the app to Slack 
-
-Now we have the main functionality of the bot programmed, we need to connect it to Slack. To create a new Slack bot, you need to go to your [Slack API app page](https://api.slack.com/apps), login and choose `Create New App`. You will be prompted to give your app a name and select a development workspace. Here I call my app `Composition Bot` and select my personal workspace. 
-
-|<img src="mySlackApp.png" width="60%" title="Create a Slack app">|
-|:--:|
-|Create a new Slack app for our bot|
-
-At your Slack app's main page, follow the steps to setup the chatbot: 
-
-1. Go to `Features > Bot Users` and `Add a Bot User`. I have both display name and default username be `composition_bot`. Turn on `Always Show My Bot as Online`. Press `Add Bot User` to add the bot. 
-2. Go to `Settings > Install App` and click `Install App to Workspace`. Authorize Slack to add the bot user. 
-3. GO to `Features > Event subscriptions`, turn it on, go to `Subscribe to Bot Events` and add the `message.im` bot user event. This event is fired when our bot receives a direct message. Scroll to the top of the page. You'll see an empty `Request URL` text box. 
-
-Let's go back to Shell now to create a new app called `slackTranslationBot`
-
-```bash
-# in Shell
-> compose slackTranslationBot
-```
-
-Paste the following code to the editor, and hit Deploy.
-
-```javascript
-composer.sequence(p => p)
-```
-
-`slackTranslationBot` currently only has an echo function that will return whatever the input is. Now, let's create a URL endpoint for triggering `slackTranslationBot`. 
-
-```bash
-# in Shell
-> webbify slackTranslationBot
-https://openwhisk.ng.bluemix.net/xxxxx....
-```
-
-The `webbify` command creates a URL for invoking a cloud function or composition. Copy of that URL, go back to the web browser and paste it to the `Request URL` text box in our Slack app's `Event subscriptions` page. Hit `Save Changes` at the bottom of the page. Now, try sending a direct message to our `composition_bot`.
-
-|<img src="hello_bot.png" width="60%" title="hello message to bot">|
-|:--:|
-|Send a test message "hello bot" to the slack bot|
-
-In Shell, enter `session get --last` to view the most recent session that was from `slackTranslationBot` as it just got triggered. View the data returned by Slack. According to Slack's [documentations](https://api.slack.com/events/message), the message is stored in `event.text`. Also, if a message is generated by the bot itself, there'll be a `event.subtype` that has the value `"bot_message"`. We do not want our bot to reply to itself, so we will add a condition in the composition code to handle that later. 
-
-The last piece we need is to create an action that can send a message back to our bot. Go to `Features > Incoming Webhooks`in the Slack app page. Turn the feature on, and add a new webhook that enables our app to post to `@composition_bot`. We can post a message by making a HTTP POST request with data `{"text": "our message"}` to the webhook. Let's create an action to do that.
-
-```bash
-# in Shell
-> new sendSlackMsg
-```
-
-Copy the below JavaScript code and paste it in the editor. Remember to replace the value of `url` to be your webhook URL. Hit `Deploy` when you're done. 
-
-```javascript
-var request = require('request');
-function main(arg){
-    return new Promise((resolve, reject) => {
-        request.post({
-            headers: {'content-type' : 'application/json'},
-            url: 'http://xxxxx',    // replace this with your webhook url
-            body: JSON.stringify({text: arg.text})
-        }, function(error, response, body){
-            if(error){
-                reject({success: false, input: arg.text, error: error});
-            }
-            resolve({success: true, input: arg.text, result: response});
-        });
-    }); 
-}
-```
-
-Now let's edit the `slackTranslationBot` app. 
-
-```bash
-# in Shell
-> edit slackTranslationBot
-```
-
-Copy and paste this code to the editor, and redeploy `slackTranslationBot`.
-
-```javascript
-composer.if(
-    p => p.event.subtype !== 'bot_message', // ignore the messages sent by the bot itself
-    composer.sequence(
-        p => ({payload: p.event.text}),   // rename data for the next action
-        'myTranslateApp',           // do translation
-        p => ({text: p.payload}),   // rename data for the next action
-        'sendSlackMsg'              // send a message back
-    )
-) 
-```
-
-Here we reuse `myTranslateApp`, and use some inline functions to rename the data for Slack. You can imagine reusing `myTranslateApp` again if we want to build the same bot for other messaging platforms like Facebook. 
-
-## Testing the Slack Bot 
-
-Let's test it by sending another test message to our slack bot. 
-
-|<img src="finishedBot.png" width="60%" title="Our translation Slack bot">|
-|:--:|
-|Now we have a translation Slack bot|
-
-Congratulations! You now have a Slack bot that can translate the user's message. The bot is completely serverless, meaning that you never pay for an idle bot (only pay per use), and the bot scales automatically. 
-
-You can use `session list --name slackTranslationBot` in Shell to look at your bot's execution history. 
-
diff --git a/docs/tutorials/translateBot/finishedBot.png b/docs/tutorials/translateBot/finishedBot.png
deleted file mode 100644
index 9314b85..0000000
--- a/docs/tutorials/translateBot/finishedBot.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/translateBot/hello_bot.png b/docs/tutorials/translateBot/hello_bot.png
deleted file mode 100644
index b6d3858..0000000
--- a/docs/tutorials/translateBot/hello_bot.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/translateBot/mySlackApp.png b/docs/tutorials/translateBot/mySlackApp.png
deleted file mode 100644
index 57928fe..0000000
--- a/docs/tutorials/translateBot/mySlackApp.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/translateBot/myTranslateApp-edit.png b/docs/tutorials/translateBot/myTranslateApp-edit.png
deleted file mode 100644
index abf9753..0000000
--- a/docs/tutorials/translateBot/myTranslateApp-edit.png
+++ /dev/null
Binary files differ
diff --git a/package.json b/package.json
index 7dad465..3c7029f 100644
--- a/package.json
+++ b/package.json
@@ -5,17 +5,18 @@
   "homepage": "https://github.com/ibm-functions/composer",
   "main": "composer.js",
   "scripts": {
-    "test": "mocha"
+    "test": "standard && mocha"
   },
   "bin": {
-    "compose": "./bin/compose"
+    "compose": "./bin/compose.js",
+    "deploy": "./bin/deploy.js"
   },
   "files": [
     "bin/",
     "composer.js",
+    "conductor.js",
     "docs/*.md",
-    "samples/",
-    "test/"
+    "samples/"
   ],
   "repository": {
     "type": "git",
@@ -26,17 +27,19 @@
     "functions",
     "serverless",
     "composer",
-    "bluemix",
     "openwhisk"
   ],
   "dependencies": {
     "minimist": "^1.2.0",
     "openwhisk": "^3.11.0",
+    "openwhisk-fqn": "^0.0.2",
     "semver": "^5.3.0",
-    "uglify-es": "^3.3.9"
+    "terser": "^3.8.2"
   },
   "devDependencies": {
-    "mocha": "^5.2.0"
+    "mocha": "^5.2.0",
+    "pre-commit": "^1.2.2",
+    "standard": "^12.0.1"
   },
   "author": {
     "name": "Olivier Tardieu",
diff --git a/samples/demo-conductor.js b/samples/demo-conductor.js
deleted file mode 100644
index 6b6df45..0000000
--- a/samples/demo-conductor.js
+++ /dev/null
@@ -1,105 +0,0 @@
-// 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.js b/samples/demo.js
index f5e3805..2b0c20f 100644
--- a/samples/demo.js
+++ b/samples/demo.js
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017 IBM Corporation
+ * Copyright 2017-2018 IBM Corporation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,6 +17,6 @@
 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' } } }))
+  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/demo.json b/samples/demo.json
index 1ecf1d8..b01f72e 100644
--- a/samples/demo.json
+++ b/samples/demo.json
@@ -1,33 +1,143 @@
 {
-    "type": "if",
-    "test": {
-        "type": "action",
-        "name": "/_/authenticate",
-        "action": {
-            "exec": {
-                "kind": "nodejs:default",
-                "code": "const main = function ({ password }) { return { value: password === 'abc123' } }"
+    "composition": {
+        "type": "let",
+        "declarations": {
+            "params": null
+        },
+        "components": [
+            {
+                "type": "finally",
+                "body": {
+                    "type": "function",
+                    "function": {
+                        "exec": {
+                            "kind": "nodejs:default",
+                            "code": "args => { params = args }"
+                        }
+                    }
+                },
+                "finalizer": {
+                    "type": "if_nosave",
+                    "test": {
+                        "type": "mask",
+                        "components": [
+                            {
+                                "type": "action",
+                                "name": "/_/authenticate",
+                                "path": ".test"
+                            }
+                        ]
+                    },
+                    "consequent": {
+                        "type": "finally",
+                        "body": {
+                            "type": "function",
+                            "function": {
+                                "exec": {
+                                    "kind": "nodejs:default",
+                                    "code": "() => params"
+                                }
+                            }
+                        },
+                        "finalizer": {
+                            "type": "mask",
+                            "components": [
+                                {
+                                    "type": "action",
+                                    "name": "/_/success",
+                                    "path": ".consequent"
+                                }
+                            ]
+                        }
+                    },
+                    "alternate": {
+                        "type": "finally",
+                        "body": {
+                            "type": "function",
+                            "function": {
+                                "exec": {
+                                    "kind": "nodejs:default",
+                                    "code": "() => params"
+                                }
+                            }
+                        },
+                        "finalizer": {
+                            "type": "mask",
+                            "components": [
+                                {
+                                    "type": "action",
+                                    "name": "/_/failure",
+                                    "path": ".alternate"
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        ],
+        "path": ""
+    },
+    "ast": {
+        "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' } }"
+                }
             }
         }
     },
-    "consequent": {
-        "type": "action",
-        "name": "/_/success",
-        "action": {
-            "exec": {
-                "kind": "nodejs:default",
-                "code": "const main = function () { return { message: 'success' } }"
+    "version": "0.7.0",
+    "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' } }"
+                }
             }
         }
-    },
-    "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
deleted file mode 100644
index f223cc4..0000000
--- a/samples/node-demo.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2017 IBM Corporation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// require the composer module
-const composer = require('@ibm-functions/composer')
-
-// define the composition
-const composition = 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' } } }))
-
-// instantiate OpenWhisk client
-const wsk = composer.util.openwhisk({ ignore_certs: true })
-
-wsk.compositions.deploy({ name: '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/composer.js b/test/composer.js
new file mode 100644
index 0000000..a5f8d6b
--- /dev/null
+++ b/test/composer.js
@@ -0,0 +1,416 @@
+/*
+ * Copyright 2017-2018 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-env mocha */
+
+'use strict'
+
+const assert = require('assert')
+const composer = require('../composer')
+
+function check (combinator, n, p, name) {
+  if (n === undefined) {
+    it('variable argument count', function () {
+      for (let i = 0; i < 5; i++) composer[combinator](...Array(i).fill('foo'))
+      for (let i = 0; i < 5; i++) composer[combinator](...Array(i).fill(() => { }))
+    })
+  } else {
+    it('argument count', function () {
+      for (let i = n; i <= (p || n); i++) composer[combinator](...Array(i).fill('foo'))
+    })
+    it('too many arguments', function () {
+      try {
+        composer[combinator](...Array((p || n) + 1).fill('foo'))
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Too many arguments'))
+      }
+    })
+    if (n > 0) {
+      it('too few arguments', function () {
+        try {
+          composer[combinator](...Array(n - 1).fill('foo'))
+          assert.fail()
+        } catch (error) {
+          assert.ok(error.message.startsWith('Invalid argument'))
+        }
+      })
+    }
+  }
+  it('combinator type', function () {
+    assert.ok(composer[combinator](...Array(n || 0).fill('foo')).type === name || combinator)
+  })
+}
+
+describe('composer', function () {
+  describe('composer.action', function () {
+    it('argument count', function () {
+      composer.action('foo')
+    })
+
+    it('too many arguments', function () {
+      try {
+        composer.action('foo', {}, 'foo')
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Too many arguments'))
+      }
+    })
+
+    it('too few arguments', function () {
+      try {
+        composer.action()
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Name must be a string'))
+      }
+    })
+
+    it('combinator type', function () {
+      assert.ok(composer.action('foo').type === 'action')
+    })
+
+    it('valid and invalid names', function () {
+      let combos = [
+        { n: 42, s: false, e: 'Name must be a string' },
+        { n: '', s: false, e: 'Name is not valid' },
+        { n: ' ', s: false, e: 'Name is not valid' },
+        { n: '/', s: false, e: 'Name is not valid' },
+        { n: '//', s: false, e: 'Name is not valid' },
+        { n: '/a', s: false, e: 'Name is not valid' },
+        { n: '/a/b/c/d', s: false, e: 'Name is not valid' },
+        { n: '/a/b/c/d/', s: false, e: 'Name is not valid' },
+        { n: 'a/b/c/d', s: false, e: 'Name is not valid' },
+        { n: '/a/ /b', s: false, e: 'Name is not valid' },
+        { n: 'a', e: false, s: '/_/a' },
+        { n: 'a/b', e: false, s: '/_/a/b' },
+        { n: 'a/b/c', e: false, s: '/a/b/c' },
+        { n: '/a/b', e: false, s: '/a/b' },
+        { n: '/a/b/c', e: false, s: '/a/b/c' }
+      ]
+      combos.forEach(({ n, s, e }) => {
+        if (s) {
+          // good cases
+          assert.ok(composer.action(n).name, s)
+        } else {
+          // error cases
+          try {
+            composer.action(n)
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith(e))
+          }
+        }
+      })
+    })
+
+    it('valid and invalid options', function () {
+      composer.action('foo', {})
+      try {
+        composer.action('foo', 42)
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+  })
+
+  describe('composer.function', function () {
+    check('function', 1)
+
+    it('function', function () {
+      composer.function(() => { })
+    })
+
+    it('string', function () {
+      composer.function('() => {}')
+    })
+
+    it('number (invalid)', function () {
+      try {
+        composer.function(42)
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+  })
+
+  describe('composer.literal', function () {
+    check('literal', 1)
+
+    it('boolean', function () {
+      composer.literal(true)
+    })
+
+    it('number', function () {
+      composer.literal(42)
+    })
+
+    it('string', function () {
+      composer.literal('foo')
+    })
+
+    it('dictionary', function () {
+      composer.literal({ foo: 42 })
+    })
+
+    it('function (invalid)', function () {
+      try {
+        composer.literal(() => { })
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+  })
+
+  describe('composer.value', function () {
+    check('value', 1)
+
+    it('boolean', function () {
+      composer.value(true)
+    })
+
+    it('number', function () {
+      composer.value(42)
+    })
+
+    it('string', function () {
+      composer.value('foo')
+    })
+
+    it('dictionary', function () {
+      composer.value({ foo: 42 })
+    })
+
+    it('function (invalid)', function () {
+      try {
+        composer.value(() => { })
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+  })
+
+  describe('composer.parse', function () {
+    it('argument count', function () {
+      composer.parse({ 'type': 'sequence', 'components': [] })
+    })
+
+    it('too many arguments', function () {
+      try {
+        composer.parse({ 'type': 'sequence', 'components': [] }, 'foo')
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Too many arguments'))
+      }
+    })
+
+    it('too few arguments', function () {
+      try {
+        composer.parse()
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+
+    it('combinator type', function () {
+      assert.ok(composer.parse({
+        'type': 'sequence',
+        'components': [{
+          'type': 'action',
+          'name': 'echo'
+        }, {
+          'type': 'action',
+          'name': 'echo'
+        }]
+      }).type === 'sequence')
+    })
+  })
+
+  describe('composer.task', function () {
+    check('task', 1, 1, 'action')
+
+    it('string', function () {
+      composer.task('isNotOne')
+    })
+
+    it('function', function () {
+      composer.task(() => { })
+    })
+
+    it('null', function () {
+      composer.task(null)
+    })
+
+    it('boolean (invalid)', function () {
+      try {
+        composer.task(false)
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+
+    it('number (invalid)', function () {
+      try {
+        composer.task(42)
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+
+    it('dictionary (invalid)', function () {
+      try {
+        composer.task({ foo: 42 })
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+  })
+
+  describe('composer.let', function () {
+    it('variable argument count', function () {
+      composer.let({})
+      composer.let({}, 'foo')
+      composer.let({}, 'foo', 'foo')
+    })
+
+    it('too few arguments', function () {
+      try {
+        composer.let()
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+
+    it('combinator type', function () {
+      assert.ok(composer.let({}).type === 'let')
+    })
+  })
+
+  describe('composer.repeat', function () {
+    it('variable argument count', function () {
+      composer.repeat(42)
+      composer.repeat(42, 'foo')
+      composer.repeat(42, 'foo', 'foo')
+    })
+
+    it('too few arguments', function () {
+      try {
+        composer.repeat()
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+
+    it('combinator type', function () {
+      assert.ok(composer.repeat(42).type === 'repeat')
+    })
+  })
+
+  describe('composer.retry', function () {
+    it('variable argument count', function () {
+      composer.retry(42)
+      composer.retry(42, 'foo')
+      composer.retry(42, 'foo', 'foo')
+    })
+
+    it('too few arguments', function () {
+      try {
+        composer.retry()
+        assert.fail()
+      } catch (error) {
+        assert.ok(error.message.startsWith('Invalid argument'))
+      }
+    })
+
+    it('combinator type', function () {
+      assert.ok(composer.retry(42).type === 'retry')
+    })
+  })
+
+  describe('composer.if', function () {
+    check('if', 2, 3)
+  })
+
+  describe('composer.if_nosave', function () {
+    check('if_nosave', 2, 3)
+  })
+
+  describe('composer.while', function () {
+    check('while', 2)
+  })
+
+  describe('composer.while_nosave', function () {
+    check('while_nosave', 2)
+  })
+
+  describe('composer.dowhile', function () {
+    check('dowhile', 2)
+  })
+
+  describe('composer.dowhile_nosave', function () {
+    check('dowhile_nosave', 2)
+  })
+
+  describe('composer.try', function () {
+    check('try', 2)
+  })
+
+  describe('composer.finally', function () {
+    check('finally', 2)
+  })
+
+  describe('composer.empty', function () {
+    check('empty', 0)
+  })
+
+  describe('composer.mask', function () {
+    check('mask')
+  })
+
+  describe('composer.async', function () {
+    check('async')
+  })
+
+  describe('composer.retain', function () {
+    check('retain')
+  })
+
+  describe('composer.retain_catch', function () {
+    check('retain_catch')
+  })
+
+  describe('composer.sequence', function () {
+    check('sequence')
+  })
+
+  describe('composer.seq', function () {
+    check('seq')
+  })
+
+  describe('composer.merge', function () {
+    check('merge')
+  })
+})
diff --git a/test/conductor.js b/test/conductor.js
new file mode 100644
index 0000000..c9ce3ed
--- /dev/null
+++ b/test/conductor.js
@@ -0,0 +1,591 @@
+/*
+ * Copyright 2017-2018 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-env mocha */
+
+'use strict'
+
+const assert = require('assert')
+const composer = require('../composer')
+const conductor = require('../conductor')
+const name = 'TestAction'
+const wsk = conductor({ 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 = (composition, params = {}, blocking = true) => wsk.compositions.deploy(Object.assign({ name }, composition.compile()), true)
+  .then(() => wsk.actions.invoke({ name, params, blocking }))
+  .then(activation => activation.response.success ? activation : Promise.reject(Object.assign(new Error(), { error: activation })))
+
+describe('composer', function () {
+  let n, x, y // dummy variables
+
+  this.timeout(60000)
+
+  before('deploy test actions', function () {
+    return define({ name: 'echo', action: 'const main = x=>x' })
+      .then(() => define({ name: 'DivideByTwo', action: 'function main({n}) { return { n: n / 2 } }' }))
+      .then(() => define({ name: 'TripleAndIncrement', action: 'function main({n}) { return { n: n * 3 + 1 } }' }))
+      .then(() => define({ name: 'isNotOne', action: 'function main({n}) { return { value: n != 1 } }' }))
+      .then(() => define({ name: 'isEven', action: 'function main({n}) { return { value: n % 2 == 0 } }' }))
+      .then(() => wsk.compositions.deploy(Object.assign({ name: '_DivideByTwo' }, composer.seq('DivideByTwo').compile()), true))
+  })
+
+  describe('blocking invocations', function () {
+    describe('actions', function () {
+      it('action must return true', function () {
+        return invoke(composer.action('isNotOne'), { n: 0 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
+      })
+
+      it('action must return false', function () {
+        return invoke(composer.action('isNotOne'), { n: 1 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: false }))
+      })
+
+      it('action must return activationId', function () {
+        return invoke(composer.async('isNotOne'), { n: 1 }).then(activation => assert.ok(activation.response.result.activationId))
+      })
+
+      it('action name must parse to fully qualified', function () {
+        let combos = [
+          { n: 42, s: false, e: 'Name must be a string' },
+          { n: '', s: false, e: 'Name is not valid' },
+          { n: ' ', s: false, e: 'Name is not valid' },
+          { n: '/', s: false, e: 'Name is not valid' },
+          { n: '//', s: false, e: 'Name is not valid' },
+          { n: '/a', s: false, e: 'Name is not valid' },
+          { n: '/a/b/c/d', s: false, e: 'Name is not valid' },
+          { n: '/a/b/c/d/', s: false, e: 'Name is not valid' },
+          { n: 'a/b/c/d', s: false, e: 'Name is not valid' },
+          { n: '/a/ /b', s: false, e: 'Name is not valid' },
+          { n: 'a', e: false, s: '/_/a' },
+          { n: 'a/b', e: false, s: '/_/a/b' },
+          { n: 'a/b/c', e: false, s: '/a/b/c' },
+          { n: '/a/b', e: false, s: '/a/b' },
+          { n: '/a/b/c', e: false, s: '/a/b/c' }
+        ]
+        combos.forEach(({ n, s, e }) => {
+          if (s) {
+            // good cases
+            assert.ok(composer.action(n).name, s)
+          } else {
+            // error cases
+            try {
+              composer.action(n)
+              assert.fail()
+            } catch (error) {
+              assert.ok(error.message.startsWith(e))
+            }
+          }
+        })
+      })
+
+      it('invalid options', function () {
+        try {
+          invoke(composer.action('foo', 42))
+          assert.fail()
+        } catch (error) {
+          assert.ok(error.message.startsWith('Invalid argument'))
+        }
+      })
+
+      it('too many arguments', function () {
+        try {
+          invoke(composer.action('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.deepStrictEqual(activation.response.result, { value: true }))
+      })
+
+      it('42', function () {
+        return invoke(composer.literal(42)).then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
+      })
+
+      it('invalid argument', function () {
+        try {
+          invoke(composer.literal(invoke))
+          assert.fail()
+        } catch (error) {
+          assert.ok(error.message.startsWith('Invalid argument'))
+        }
+      })
+
+      it('too many arguments', function () {
+        try {
+          invoke(composer.literal('foo', 'foo'))
+          assert.fail()
+        } catch (error) {
+          assert.ok(error.message.startsWith('Too many arguments'))
+        }
+      })
+    })
+
+    describe('functions', function () {
+      it('function must return true', function () {
+        return invoke(composer.function(({ n }) => n % 2 === 0), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
+      })
+
+      it('function must return false', function () {
+        return invoke(composer.function(function ({ n }) { return n % 2 === 0 }), { n: 3 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: false }))
+      })
+
+      it('function must fail', function () {
+        return invoke(composer.function(() => n)).then(() => assert.fail(), activation => assert.ok(activation.error.response.result.error))
+      })
+
+      it('function must throw', function () {
+        return invoke(composer.function(() => ({ error: 'foo', n: 42 }))).then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result, { error: 'foo' }))
+      })
+
+      it('function must mutate params', function () {
+        return invoke(composer.function(params => { params.foo = 'foo' }), { n: 42 }).then(activation => assert.deepStrictEqual(activation.response.result, { foo: 'foo', n: 42 }))
+      })
+
+      it('function as string', function () {
+        return invoke(composer.function('({ n }) => n % 2 === 0'), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
+      })
+
+      it('function may return a promise', function () {
+        return invoke(composer.function(({ n }) => Promise.resolve(n % 2 === 0)), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
+      })
+
+      it('invalid argument', function () {
+        try {
+          invoke(composer.function(42))
+          assert.fail()
+        } catch (error) {
+          assert.ok(error.message.startsWith('Invalid argument'))
+        }
+      })
+
+      it('too many arguments', function () {
+        try {
+          invoke(composer.function(() => n, () => { }))
+          assert.fail()
+        } catch (error) {
+          assert.ok(error.message.startsWith('Too many arguments'))
+        }
+      })
+    })
+
+    describe('deserialize', function () {
+      it('should deserialize a serialized composition', function () {
+        const json = {
+          'type': 'sequence',
+          'components': [{
+            'type': 'action',
+            'name': 'echo'
+          }, {
+            'type': 'action',
+            'name': 'echo'
+          }]
+        }
+        return invoke(composer.parse(json), { message: 'hi' }).then(activation => assert.deepStrictEqual(activation.response.result, { message: 'hi' }))
+      })
+    })
+
+    describe('tasks', function () {
+      describe('action tasks', function () {
+        it('action must return true', function () {
+          return invoke(composer.task('isNotOne'), { n: 0 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
+        })
+      })
+
+      describe('function tasks', function () {
+        it('function must return true', function () {
+          return invoke(composer.task(({ n }) => n % 2 === 0), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
+        })
+      })
+
+      describe('null task', function () {
+        it('null task must return input', function () {
+          return invoke(composer.task(null), { foo: 'foo' }).then(activation => assert.deepStrictEqual(activation.response.result, { foo: 'foo' }))
+        })
+
+        it('null task must fail on error input', function () {
+          return invoke(composer.task(null), { error: 'foo' }).then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result, { error: 'foo' }))
+        })
+      })
+
+      describe('invalid tasks', function () {
+        it('a Boolean is not a valid task', function () {
+          try {
+            invoke(composer.task(false))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Invalid argument'))
+          }
+        })
+
+        it('a number is not a valid task', function () {
+          try {
+            invoke(composer.task(42))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Invalid argument'))
+          }
+        })
+
+        it('a dictionary is not a valid task', function () {
+          try {
+            invoke(composer.task({ foo: 'foo' }))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Invalid argument'))
+          }
+        })
+      })
+
+      it('too many arguments', function () {
+        try {
+          invoke(composer.task('foo', 'foo'))
+          assert.fail()
+        } catch (error) {
+          assert.ok(error.message.startsWith('Too many arguments'))
+        }
+      })
+    })
+
+    describe('combinators', function () {
+      describe('sequence', function () {
+        it('flat', function () {
+          return invoke(composer.sequence('TripleAndIncrement', 'DivideByTwo', 'DivideByTwo'), { n: 5 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
+        })
+
+        it('nested right', function () {
+          return invoke(composer.sequence('TripleAndIncrement', composer.sequence('DivideByTwo', 'DivideByTwo')), { n: 5 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
+        })
+
+        it('nested left', function () {
+          return invoke(composer.sequence(composer.sequence('TripleAndIncrement', 'DivideByTwo'), 'DivideByTwo'), { n: 5 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
+        })
+
+        it('seq', function () {
+          return invoke(composer.seq('TripleAndIncrement', 'DivideByTwo', 'DivideByTwo'), { n: 5 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
+        })
+      })
+
+      describe('if', function () {
+        it('condition = true', function () {
+          return invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement'), { n: 4 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 2 }))
+        })
+
+        it('condition = false', function () {
+          return invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement'), { n: 3 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 10 }))
+        })
+
+        it('condition = true, then branch only', function () {
+          return invoke(composer.if('isEven', 'DivideByTwo'), { n: 4 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 2 }))
+        })
+
+        it('condition = false, then branch only', function () {
+          return invoke(composer.if('isEven', 'DivideByTwo'), { n: 3 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 3 }))
+        })
+
+        it('condition = true, nosave option', function () {
+          return invoke(composer.if_nosave('isEven', params => { params.then = true }, params => { params.else = true }), { n: 2 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: true, then: true }))
+        })
+
+        it('condition = false, nosave option', function () {
+          return invoke(composer.if_nosave('isEven', params => { params.then = true }, params => { params.else = true }), { n: 3 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: false, else: true }))
+        })
+
+        it('too many arguments', function () {
+          try {
+            invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement', 'TripleAndIncrement'))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Too many arguments'))
+          }
+        })
+      })
+
+      describe('while', function () {
+        it('a few iterations', function () {
+          return invoke(composer.while('isNotOne', ({ n }) => ({ n: n - 1 })), { n: 4 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
+        })
+
+        it('no iteration', function () {
+          return invoke(composer.while(() => false, ({ n }) => ({ n: n - 1 })), { n: 1 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
+        })
+
+        it('nosave option', function () {
+          return invoke(composer.while_nosave(({ n }) => ({ n, value: n !== 1 }), ({ n }) => ({ n: n - 1 })), { n: 4 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: false, n: 1 }))
+        })
+
+        it('too many arguments', 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('Too many arguments'))
+          }
+        })
+      })
+
+      describe('dowhile', function () {
+        it('a few iterations', function () {
+          return invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), 'isNotOne'), { n: 4 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
+        })
+
+        it('one iteration', function () {
+          return invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), () => false), { n: 1 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 0 }))
+        })
+
+        it('nosave option', function () {
+          return invoke(composer.dowhile_nosave(({ n }) => ({ n: n - 1 }), ({ n }) => ({ n, value: n !== 1 })), { n: 4 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: false, n: 1 }))
+        })
+
+        it('too many arguments', 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('Too many arguments'))
+          }
+        })
+      })
+
+      describe('try', function () {
+        it('no error', function () {
+          return invoke(composer.try(() => true, error => ({ message: error.error })))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
+        })
+
+        it('error', function () {
+          return invoke(composer.try(() => ({ error: 'foo' }), error => ({ message: error.error })))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
+        })
+
+        it('try must throw', function () {
+          return invoke(composer.try(composer.task(null), error => ({ message: error.error })), { error: 'foo' })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
+        })
+
+        it('while must throw', function () {
+          return invoke(composer.try(composer.while(composer.literal(false), null), error => ({ message: error.error })), { error: 'foo' })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
+        })
+
+        it('if must throw', function () {
+          return invoke(composer.try(composer.if(composer.literal(false), null), error => ({ message: error.error })), { error: 'foo' })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
+        })
+
+        it('retain', function () {
+          return invoke(composer.retain(composer.try(() => ({ p: 4 }), null)), { n: 3 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { params: { n: 3 }, result: { p: 4 } }))
+        })
+
+        it('too many arguments', function () {
+          try {
+            invoke(composer.try('isNotOne', 'isNotOne', 'isNotOne'))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Too many arguments'))
+          }
+        })
+      })
+
+      describe('finally', function () {
+        it('no error', function () {
+          return invoke(composer.finally(() => true, params => ({ params })))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { params: { value: true } }))
+        })
+
+        it('error', function () {
+          return invoke(composer.finally(() => ({ error: 'foo' }), params => ({ params })))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { params: { error: 'foo' } }))
+        })
+
+        it('too many arguments', function () {
+          try {
+            invoke(composer.finally('isNotOne', 'isNotOne', 'isNotOne'))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Too many arguments'))
+          }
+        })
+      })
+
+      describe('let', function () {
+        it('one variable', function () {
+          return invoke(composer.let({ x: 42 }, () => x))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
+        })
+
+        it('masking', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, () => x)))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 69 }))
+        })
+
+        it('two variables', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ y: 69 }, () => x + y)))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 111 }))
+        })
+
+        it('two variables combined', function () {
+          return invoke(composer.let({ x: 42, y: 69 }, () => x + y))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 111 }))
+        })
+
+        it('scoping', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, () => x), ({ value }) => value + x))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 111 }))
+        })
+
+        it('invalid argument', function () {
+          try {
+            invoke(composer.let(invoke))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Invalid argument'))
+          }
+        })
+      })
+
+      describe('mask', function () {
+        it('let/let/mask', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, composer.mask(() => x))))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
+        })
+
+        it('let/mask/let', function () {
+          return invoke(composer.let({ x: 42 }, composer.mask(composer.let({ x: 69 }, () => x))))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 69 }))
+        })
+
+        it('let/let/try/mask', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
+            composer.try(composer.mask(() => x), () => { }))))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
+        })
+
+        it('let/let/let/mask', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
+            composer.let({ x: -1 }, composer.mask(() => x)))))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 69 }))
+        })
+
+        it('let/let/let/mask/mask', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
+            composer.let({ x: -1 }, composer.mask(composer.mask(() => x))))))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
+        })
+
+        it('let/let/mask/let/mask', function () {
+          return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
+            composer.mask(composer.let({ x: -1 }, composer.mask(() => x))))))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
+        })
+      })
+
+      describe('retain', function () {
+        it('base case', function () {
+          return invoke(composer.retain('TripleAndIncrement'), { n: 3 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { params: { n: 3 }, result: { n: 10 } }))
+        })
+
+        it('throw error', function () {
+          return invoke(composer.retain(() => ({ error: 'foo' })), { n: 3 })
+            .then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result, { error: 'foo' }))
+        })
+
+        it('catch error', function () {
+          return invoke(composer.retain_catch(() => ({ error: 'foo' })), { n: 3 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { params: { n: 3 }, result: { error: 'foo' } }))
+        })
+      })
+
+      describe('merge', function () {
+        it('base case', function () {
+          return invoke(composer.merge('TripleAndIncrement'), { n: 3, p: 4 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 10, p: 4 }))
+        })
+      })
+
+      describe('repeat', function () {
+        it('a few iterations', function () {
+          return invoke(composer.repeat(3, 'DivideByTwo'), { n: 8 })
+            .then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
+        })
+
+        it('invalid argument', function () {
+          try {
+            invoke(composer.repeat('foo'))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Invalid argument'))
+          }
+        })
+      })
+
+      describe('retry', function () {
+        it('success', function () {
+          return invoke(composer.let({ x: 2 }, composer.retry(2, () => x-- > 0 ? { error: 'foo' } : 42)))
+            .then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
+        })
+
+        it('failure', function () {
+          return invoke(composer.let({ x: 2 }, composer.retry(1, () => x-- > 0 ? { error: 'foo' } : 42)))
+            .then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result.error, 'foo'))
+        })
+
+        it('invalid argument', function () {
+          try {
+            invoke(composer.retry('foo'))
+            assert.fail()
+          } catch (error) {
+            assert.ok(error.message.startsWith('Invalid argument'))
+          }
+        })
+      })
+    })
+  })
+
+  describe('compositions', function () {
+    describe('collatz', function () {
+      it('composition must return { n: 1 }', function () {
+        return invoke(composer.while('isNotOne', composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement')), { n: 5 })
+          .then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
+      })
+    })
+  })
+})
diff --git a/test/test.js b/test/test.js
deleted file mode 100644
index 8d72a4f..0000000
--- a/test/test.js
+++ /dev/null
@@ -1,559 +0,0 @@
-const assert = require('assert')
-const composer = require('../composer')
-const name = 'TestAction'
-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 = (composition, params = {}, blocking = true) => wsk.compositions.deploy({ name, composition }).then(() => wsk.actions.invoke({ name, params, blocking }))
-
-describe('composer', function () {
-    this.timeout(60000)
-
-    before('deploy test actions', function () {
-        return define({ name: 'echo', action: 'const main = x=>x' })
-            .then(() => define({ name: 'DivideByTwo', action: 'function main({n}) { return { n: n / 2 } }' }))
-            .then(() => define({ name: 'TripleAndIncrement', action: 'function main({n}) { return { n: n * 3 + 1 } }' }))
-            .then(() => define({ name: 'isNotOne', action: 'function main({n}) { return { value: n != 1 } }' }))
-            .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 () {
-                return invoke(composer.action('isNotOne'), { n: 0 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
-            })
-
-            it('action must return false', function () {
-                return invoke(composer.action('isNotOne'), { n: 1 }).then(activation => assert.deepEqual(activation.response.result, { value: false }))
-            })
-
-            it('action must return activationId', function () {
-                return invoke(composer.async('isNotOne'), { n: 1 }).then(activation => assert.ok(activation.response.result.activationId))
-            })
-
-            it('action name must parse to fully qualified', function () {
-                let combos = [
-                    { n: 42, s: false, e: 'Name must be a string' },
-                    { n: '', s: false, e: 'Name is not valid' },
-                    { n: ' ', s: false, e: 'Name is not valid' },
-                    { n: '/', s: false, e: 'Name is not valid' },
-                    { n: '//', s: false, e: 'Name is not valid' },
-                    { n: '/a', s: false, e: 'Name is not valid' },
-                    { n: '/a/b/c/d', s: false, e: 'Name is not valid' },
-                    { n: '/a/b/c/d/', s: false, e: 'Name is not valid' },
-                    { n: 'a/b/c/d', s: false, e: 'Name is not valid' },
-                    { n: '/a/ /b', s: false, e: 'Name is not valid' },
-                    { n: 'a', e: false, s: '/_/a' },
-                    { n: 'a/b', e: false, s: '/_/a/b' },
-                    { n: 'a/b/c', e: false, s: '/a/b/c' },
-                    { n: '/a/b', e: false, s: '/a/b' },
-                    { n: '/a/b/c', e: false, s: '/a/b/c' }
-                ]
-                combos.forEach(({ n, s, e }) => {
-                    if (s) {
-                        // good cases
-                        assert.ok(composer.action(n).name, s)
-                    } else {
-                        // error cases
-                        try {
-                            composer.action(n)
-                            assert.fail()
-                        } catch (error) {
-                            assert.ok(error.message == e)
-                        }
-                    }
-                })
-            })
-
-            it('invalid options', function () {
-                try {
-                    invoke(composer.action('foo', 42))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid argument'))
-                }
-            })
-
-            it('too many arguments', function () {
-                try {
-                    invoke(composer.action('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 }))
-            })
-
-            it('42', function () {
-                return invoke(composer.literal(42)).then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
-            })
-
-            it('invalid argument', function () {
-                try {
-                    invoke(composer.literal(invoke))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid argument'))
-                }
-            })
-
-            it('too many arguments', function () {
-                try {
-                    invoke(composer.literal('foo', 'foo'))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Too many arguments'))
-                }
-            })
-        })
-
-        describe('functions', function () {
-            it('function must return true', function () {
-                return invoke(composer.function(({ n }) => n % 2 === 0), { n: 4 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
-            })
-
-            it('function must return false', function () {
-                return invoke(composer.function(function ({ n }) { return n % 2 === 0 }), { n: 3 }).then(activation => assert.deepEqual(activation.response.result, { value: false }))
-            })
-
-            it('function must fail', function () {
-                return invoke(composer.function(() => n)).then(() => assert.fail(), activation => assert.ok(activation.error.response.result.error.startsWith('An exception was caught')))
-            })
-
-            it('function must throw', function () {
-                return invoke(composer.function(() => ({ error: 'foo', n: 42 }))).then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result, { error: 'foo' }))
-            })
-
-            it('function must mutate params', function () {
-                return invoke(composer.function(params => { params.foo = 'foo' }), { n: 42 }).then(activation => assert.deepEqual(activation.response.result, { foo: 'foo', n: 42 }))
-            })
-
-            it('function as string', function () {
-                return invoke(composer.function('({ n }) => n % 2 === 0'), { n: 4 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
-            })
-
-            it('function may return a promise', function () {
-                return invoke(composer.function(({ n }) => Promise.resolve(n % 2 === 0)), { n: 4 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
-            })
-
-            it('invalid argument', function () {
-                try {
-                    invoke(composer.function(42))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Invalid argument'))
-                }
-            })
-
-            it('too many arguments', function () {
-                try {
-                    invoke(composer.function(() => n, () => { }))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Too many arguments'))
-                }
-            })
-        })
-
-        describe('deserialize', function () {
-            it('should deserialize a serialized composition', function () {
-                const json = {
-                    "type": "sequence",
-                    "components": [{
-                        "type": "action",
-                        "name": "echo"
-                    }, {
-                        "type": "action",
-                        "name": "echo"
-                    }]
-                }
-                return invoke(composer.util.deserialize(json), { message: 'hi' }).then(activation => assert.deepEqual(activation.response.result, { message: 'hi' }))
-            })
-        })
-
-        describe('tasks', function () {
-            describe('action tasks', function () {
-                it('action must return true', function () {
-                    return invoke(composer.task('isNotOne'), { n: 0 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
-                })
-            })
-
-            describe('function tasks', function () {
-                it('function must return true', function () {
-                    return invoke(composer.task(({ n }) => n % 2 === 0), { n: 4 }).then(activation => assert.deepEqual(activation.response.result, { value: true }))
-                })
-            })
-
-            describe('null task', function () {
-                it('null task must return input', function () {
-                    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(null), { error: 'foo' }).then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result, { error: 'foo' }))
-                })
-            })
-
-            describe('invalid tasks', function () {
-                it('a Boolean is not a valid task', function () {
-                    try {
-                        invoke(composer.task(false))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid argument'))
-                    }
-                })
-
-                it('a number is not a valid task', function () {
-                    try {
-                        invoke(composer.task(42))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid argument'))
-                    }
-                })
-
-                it('a dictionary is not a valid task', function () {
-                    try {
-                        invoke(composer.task({ foo: 'foo' }))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid argument'))
-                    }
-                })
-            })
-
-            it('too many arguments', function () {
-                try {
-                    invoke(composer.task('foo', 'foo'))
-                    assert.fail()
-                } catch (error) {
-                    assert.ok(error.message.startsWith('Too many arguments'))
-                }
-            })
-        })
-
-        describe('combinators', function () {
-            describe('sequence', function () {
-                it('flat', function () {
-                    return invoke(composer.sequence('TripleAndIncrement', 'DivideByTwo', 'DivideByTwo'), { n: 5 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 4 }))
-                })
-
-                it('nested right', function () {
-                    return invoke(composer.sequence('TripleAndIncrement', composer.sequence('DivideByTwo', 'DivideByTwo')), { n: 5 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 4 }))
-                })
-
-                it('nested left', function () {
-                    return invoke(composer.sequence(composer.sequence('TripleAndIncrement', 'DivideByTwo'), 'DivideByTwo'), { n: 5 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 4 }))
-                })
-
-                it('seq', function () {
-                    return invoke(composer.seq('TripleAndIncrement', 'DivideByTwo', 'DivideByTwo'), { n: 5 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 4 }))
-                })
-            })
-
-            describe('if', function () {
-                it('condition = true', function () {
-                    return invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement'), { n: 4 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 2 }))
-                })
-
-                it('condition = false', function () {
-                    return invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement'), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 10 }))
-                })
-
-                it('condition = true, then branch only', function () {
-                    return invoke(composer.if('isEven', 'DivideByTwo'), { n: 4 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 2 }))
-                })
-
-                it('condition = false, then branch only', function () {
-                    return invoke(composer.if('isEven', 'DivideByTwo'), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 3 }))
-                })
-
-                it('condition = true, nosave option', function () {
-                    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_nosave('isEven', params => { params.then = true }, params => { params.else = true }), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { value: false, else: true }))
-                })
-
-                it('too many arguments', function () {
-                    try {
-                        invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement', 'TripleAndIncrement'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Too many arguments'))
-                    }
-                })
-            })
-
-            describe('while', function () {
-                it('a few iterations', function () {
-                    return invoke(composer.while('isNotOne', ({ n }) => ({ n: n - 1 })), { n: 4 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 1 }))
-                })
-
-                it('no iteration', function () {
-                    return invoke(composer.while(() => false, ({ n }) => ({ n: n - 1 })), { n: 1 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 1 }))
-                })
-
-                it('nosave option', function () {
-                    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('too many arguments', 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('Too many arguments'))
-                    }
-                })
-            })
-
-            describe('dowhile', function () {
-                it('a few iterations', function () {
-                    return invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), 'isNotOne'), { n: 4 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 1 }))
-                })
-
-                it('one iteration', function () {
-                    return invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), () => false), { n: 1 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 0 }))
-                })
-
-                it('nosave option', function () {
-                    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('too many arguments', 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('Too many arguments'))
-                    }
-                })
-            })
-
-            describe('try', function () {
-                it('no error', function () {
-                    return invoke(composer.try(() => true, error => ({ message: error.error })))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: true }))
-                })
-
-                it('error', function () {
-                    return invoke(composer.try(() => ({ error: 'foo' }), error => ({ message: error.error })))
-                        .then(activation => assert.deepEqual(activation.response.result, { message: 'foo' }))
-                })
-
-                it('try must throw', function () {
-                    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), 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), 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 }), null)), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { params: { n: 3 }, result: { p: 4 } }))
-                })
-
-                it('too many arguments', function () {
-                    try {
-                        invoke(composer.try('isNotOne', 'isNotOne', 'isNotOne'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Too many arguments'))
-                    }
-                })
-            })
-
-            describe('finally', function () {
-                it('no error', function () {
-                    return invoke(composer.finally(() => true, params => ({ params })))
-                        .then(activation => assert.deepEqual(activation.response.result, { params: { value: true } }))
-                })
-
-                it('error', function () {
-                    return invoke(composer.finally(() => ({ error: 'foo' }), params => ({ params })))
-                        .then(activation => assert.deepEqual(activation.response.result, { params: { error: 'foo' } }))
-                })
-
-                it('too many arguments', function () {
-                    try {
-                        invoke(composer.finally('isNotOne', 'isNotOne', 'isNotOne'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Too many arguments'))
-                    }
-                })
-            })
-
-            describe('let', function () {
-                it('one variable', function () {
-                    return invoke(composer.let({ x: 42 }, () => x))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
-                })
-
-                it('masking', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, () => x)))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 69 }))
-                })
-
-                it('two variables', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ y: 69 }, () => x + y)))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 111 }))
-                })
-
-                it('two variables combined', function () {
-                    return invoke(composer.let({ x: 42, y: 69 }, () => x + y))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 111 }))
-                })
-
-                it('scoping', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, () => x), ({ value }) => value + x))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 111 }))
-                })
-
-                it('invalid argument', function () {
-                    try {
-                        invoke(composer.let(invoke))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid argument'))
-                    }
-                })
-            })
-
-            describe('mask', function () {
-                it('let/let/mask', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, composer.mask(() => x))))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
-                })
-
-                it('let/mask/let', function () {
-                    return invoke(composer.let({ x: 42 }, composer.mask(composer.let({ x: 69 }, () => x))))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 69 }))
-                })
-
-                it('let/let/try/mask', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
-                        composer.try(composer.mask(() => x), () => { }))))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
-                })
-
-                it('let/let/let/mask', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
-                        composer.let({ x: -1 }, composer.mask(() => x)))))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 69 }))
-                })
-
-                it('let/let/let/mask/mask', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
-                        composer.let({ x: -1 }, composer.mask(composer.mask(() => x))))))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
-                })
-
-                it('let/let/mask/let/mask', function () {
-                    return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
-                        composer.mask(composer.let({ x: -1 }, composer.mask(() => x))))))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
-                })
-            })
-
-            describe('retain', function () {
-                it('base case', function () {
-                    return invoke(composer.retain('TripleAndIncrement'), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { params: { n: 3 }, result: { n: 10 } }))
-                })
-
-                it('throw error', function () {
-                    return invoke(composer.retain(() => ({ error: 'foo' })), { n: 3 })
-                        .then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result, { error: 'foo' }))
-                })
-
-                it('catch error', function () {
-                    return invoke(composer.retain_catch(() => ({ error: 'foo' })), { n: 3 })
-                        .then(activation => assert.deepEqual(activation.response.result, { params: { n: 3 }, result: { error: 'foo' } }))
-                })
-            })
-
-            describe('repeat', function () {
-                it('a few iterations', function () {
-                    return invoke(composer.repeat(3, 'DivideByTwo'), { n: 8 })
-                        .then(activation => assert.deepEqual(activation.response.result, { n: 1 }))
-                })
-
-                it('invalid argument', function () {
-                    try {
-                        invoke(composer.repeat('foo'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid argument'))
-                    }
-                })
-            })
-
-            describe('retry', function () {
-                it('success', function () {
-                    return invoke(composer.let({ x: 2 }, composer.retry(2, () => x-- > 0 ? { error: 'foo' } : 42)))
-                        .then(activation => assert.deepEqual(activation.response.result, { value: 42 }))
-                })
-
-                it('failure', function () {
-                    return invoke(composer.let({ x: 2 }, composer.retry(1, () => x-- > 0 ? { error: 'foo' } : 42)))
-                        .then(() => assert.fail(), activation => assert.deepEqual(activation.error.response.result.error, 'foo'))
-
-                })
-
-                it('invalid argument', function () {
-                    try {
-                        invoke(composer.retry('foo'))
-                        assert.fail()
-                    } catch (error) {
-                        assert.ok(error.message.startsWith('Invalid argument'))
-                    }
-                })
-            })
-        })
-    })
-
-    describe('compositions', function () {
-        describe('collatz', function () {
-            it('composition must return { n: 1 }', function () {
-                return invoke(composer.while('isNotOne', composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement')), { n: 5 })
-                    .then(activation => assert.deepEqual(activation.response.result, { n: 1 }))
-            })
-        })
-    })
-})
diff --git a/travis/runtimes.json b/travis/runtimes.json
new file mode 100644
index 0000000..f565907
--- /dev/null
+++ b/travis/runtimes.json
@@ -0,0 +1,30 @@
+{
+    "runtimes": {
+        "nodejs": [
+            {
+                "kind": "nodejs",
+                "image": {
+                    "prefix": "openwhisk",
+                    "name": "nodejsaction",
+                    "tag": "latest"
+                },
+                "deprecated": true
+            },
+            {
+                "kind": "nodejs:6",
+                "default": true,
+                "image": {
+                    "prefix": "openwhisk",
+                    "name": "nodejs6action",
+                    "tag": "latest"
+                },
+                "deprecated": false,
+                "stemCells": [{
+                    "count": 2,
+                    "memory": "256 MB"
+                }]
+            }
+        ]
+    },
+    "blackboxes": []
+}
diff --git a/travis/setup.sh b/travis/setup.sh
index 138c3a6..2b69f5b 100755
--- a/travis/setup.sh
+++ b/travis/setup.sh
@@ -14,13 +14,13 @@
 cd openwhisk
 ./tools/travis/setup.sh
 
+cp $SCRIPTDIR/runtimes.json $WHISKDIR/ansible/files
+
 # Pull down images
 docker pull openwhisk/controller
 docker tag openwhisk/controller ${IMAGE_PREFIX}/controller
 docker pull openwhisk/invoker
 docker tag openwhisk/invoker ${IMAGE_PREFIX}/invoker
-docker pull openwhisk/nodejs6action
-docker tag openwhisk/nodejs6action ${IMAGE_PREFIX}/nodejs6action
 
 # Deploy OpenWhisk
 cd $WHISKDIR/ansible
@@ -32,13 +32,9 @@
 $ANSIBLE_CMD wipe.yml
 $ANSIBLE_CMD openwhisk.yml -e cli_installation_mode=remote -e limit_invocations_per_minute=600
 
-# Deploy Redis
-docker run -d -p 6379:6379 --name redis redis:3.2
-
 docker images
 docker ps
 
-cat $WHISKDIR/whisk.properties
 curl -s -k https://172.17.0.1 | jq .
 curl -s -k https://172.17.0.1/api/v1 | jq .