diff --git a/changelog.md b/changelog.md
index 06d00ff67..725924e7e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,17 @@
 # Change Log
 
+## 0.2.2
+
+Add support for Slack Interactive Messages.
+
+Add example of Slack button application that provides a bot that uses interactive messages.
+
+New functionality in Slack bot: Botkit will track spawned Slack bots and route incoming webhooks to pre-existing RTM bots. This enables RTM bots to reply to interactive messages and slash commands.
+
+## 0.2.1
+
+Improves Slack RTM reconnects thanks to @selfcontained [PR #274](https://github.com/howdyai/botkit/pull/274)
+
 ## 0.2
 
 Adds support for Twilio IP Messenging bots
diff --git a/examples/slackbutton_bot_interactivemsg.js b/examples/slackbutton_bot_interactivemsg.js
new file mode 100644
index 000000000..2874a2edb
--- /dev/null
+++ b/examples/slackbutton_bot_interactivemsg.js
@@ -0,0 +1,332 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+           ______     ______     ______   __  __     __     ______
+          /\  == \   /\  __ \   /\__  _\ /\ \/ /    /\ \   /\__  _\
+          \ \  __<   \ \ \/\ \  \/_/\ \/ \ \  _"-.  \ \ \  \/_/\ \/
+           \ \_____\  \ \_____\    \ \_\  \ \_\ \_\  \ \_\    \ \_\
+            \/_____/   \/_____/     \/_/   \/_/\/_/   \/_/     \/_/
+
+
+This is a sample Slack Button application that adds a bot to one or many slack teams.
+
+# RUN THE APP:
+  Create a Slack app. Make sure to configure the bot user!
+    -> https://api.slack.com/applications/new
+    -> Add the Redirect URI: http://localhost:3000/oauth
+  Run your bot from the command line:
+    clientId=<my client id> clientSecret=<my client secret> port=3000 node slackbutton_bot_interactivemsg.js
+# USE THE APP
+  Add the app to your Slack by visiting the login page:
+    -> http://localhost:3000/login
+  After you've added the app, try talking to your bot!
+# EXTEND THE APP:
+  Botkit has many features for building cool and useful bots!
+  Read all about it here:
+    -> http://howdy.ai/botkit
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+/* Uses the slack button feature to offer a real time bot to multiple teams */
+var Botkit = require('../lib/Botkit.js');
+
+if (!process.env.clientId || !process.env.clientSecret || !process.env.port) {
+  console.log('Error: Specify clientId clientSecret and port in environment');
+  process.exit(1);
+}
+
+
+var controller = Botkit.slackbot({
+  // interactive_replies: true, // tells botkit to send button clicks into conversations
+  json_file_store: './db_slackbutton_bot/',
+}).configureSlackApp(
+  {
+    clientId: process.env.clientId,
+    clientSecret: process.env.clientSecret,
+    scopes: ['bot'],
+  }
+);
+
+controller.setupWebserver(process.env.port,function(err,webserver) {
+  controller.createWebhookEndpoints(controller.webserver);
+
+  controller.createOauthEndpoints(controller.webserver,function(err,req,res) {
+    if (err) {
+      res.status(500).send('ERROR: ' + err);
+    } else {
+      res.send('Success!');
+    }
+  });
+});
+
+
+// just a simple way to make sure we don't
+// connect to the RTM twice for the same team
+var _bots = {};
+function trackBot(bot) {
+  _bots[bot.config.token] = bot;
+}
+
+
+controller.on('interactive_message_callback', function(bot, message) {
+
+    var ids = message.callback_id.split(/\-/);
+    var user_id = ids[0];
+    var item_id = ids[1];
+
+    controller.storage.users.get(user_id, function(err, user) {
+
+        if (!user) {
+            user = {
+                id: user_id,
+                list: []
+            }
+        }
+
+        for (var x = 0; x < user.list.length; x++) {
+            if (user.list[x].id == item_id) {
+                if (message.actions[0].value=='flag') {
+                    user.list[x].flagged = !user.list[x].flagged;
+                }
+                if (message.actions[0].value=='delete') {
+                    user.list.splice(x,1);
+                }
+            }
+        }
+
+
+        var reply = {
+            text: 'Here is <@' + user_id + '>s list:',
+            attachments: [],
+        }
+
+        for (var x = 0; x < user.list.length; x++) {
+            reply.attachments.push({
+                title: user.list[x].text + (user.list[x].flagged? ' *FLAGGED*' : ''),
+                callback_id: user_id + '-' + user.list[x].id,
+                attachment_type: 'default',
+                actions: [
+                    {
+                        "name":"flag",
+                        "text": ":waving_black_flag: Flag",
+                        "value": "flag",
+                        "type": "button",
+                    },
+                    {
+                       "text": "Delete",
+                        "name": "delete",
+                        "value": "delete",
+                        "style": "danger",
+                        "type": "button",
+                        "confirm": {
+                          "title": "Are you sure?",
+                          "text": "This will do something!",
+                          "ok_text": "Yes",
+                          "dismiss_text": "No"
+                        }
+                    }
+                ]
+            })
+        }
+
+        bot.replyInteractive(message, reply);
+        controller.storage.users.save(user);
+
+
+    });
+
+});
+
+
+controller.on('create_bot',function(bot,config) {
+
+  if (_bots[bot.config.token]) {
+    // already online! do nothing.
+  } else {
+    bot.startRTM(function(err) {
+
+      if (!err) {
+        trackBot(bot);
+      }
+
+      bot.startPrivateConversation({user: config.createdBy},function(err,convo) {
+        if (err) {
+          console.log(err);
+        } else {
+          convo.say('I am a bot that has just joined your team');
+          convo.say('You must now /invite me to a channel so that I can be of use!');
+        }
+      });
+
+    });
+  }
+
+});
+
+
+// Handle events related to the websocket connection to Slack
+controller.on('rtm_open',function(bot) {
+  console.log('** The RTM api just connected!');
+});
+
+controller.on('rtm_close',function(bot) {
+  console.log('** The RTM api just closed');
+  // you may want to attempt to re-open
+});
+
+
+controller.hears(['add (.*)'],'direct_mention,direct_message',function(bot,message) {
+
+    controller.storage.users.get(message.user, function(err, user) {
+
+        if (!user) {
+            user = {
+                id: message.user,
+                list: []
+            }
+        }
+
+        user.list.push({
+            id: message.ts,
+            text: message.match[1],
+        });
+
+        bot.reply(message,'Added to list. Say `list` to view or manage list.');
+
+        controller.storage.users.save(user);
+
+    });
+});
+
+
+controller.hears(['list','tasks'],'direct_mention,direct_message',function(bot,message) {
+
+    controller.storage.users.get(message.user, function(err, user) {
+
+        if (!user) {
+            user = {
+                id: message.user,
+                list: []
+            }
+        }
+
+        if (!user.list || !user.list.length) {
+            user.list = [
+                {
+                    'id': 1,
+                    'text': 'Test Item 1'
+                },
+                {
+                    'id': 2,
+                    'text': 'Test Item 2'
+                },
+                {
+                    'id': 3,
+                    'text': 'Test Item 3'
+                }
+            ]
+        }
+
+        var reply = {
+            text: 'Here is your list. Say `add <item>` to add items.',
+            attachments: [],
+        }
+
+        for (var x = 0; x < user.list.length; x++) {
+            reply.attachments.push({
+                title: user.list[x].text + (user.list[x].flagged? ' *FLAGGED*' : ''),
+                callback_id: message.user + '-' + user.list[x].id,
+                attachment_type: 'default',
+                actions: [
+                    {
+                        "name":"flag",
+                        "text": ":waving_black_flag: Flag",
+                        "value": "flag",
+                        "type": "button",
+                    },
+                    {
+                       "text": "Delete",
+                        "name": "delete",
+                        "value": "delete",
+                        "style": "danger",
+                        "type": "button",
+                        "confirm": {
+                          "title": "Are you sure?",
+                          "text": "This will do something!",
+                          "ok_text": "Yes",
+                          "dismiss_text": "No"
+                        }
+                    }
+                ]
+            })
+        }
+
+        bot.reply(message, reply);
+
+        controller.storage.users.save(user);
+
+    });
+
+});
+
+controller.hears('interactive', 'direct_message', function(bot, message) {
+
+    bot.reply(message, {
+        attachments:[
+            {
+                title: 'Do you want to interact with my buttons?',
+                callback_id: '123',
+                attachment_type: 'default',
+                actions: [
+                    {
+                        "name":"yes",
+                        "text": "Yes",
+                        "value": "yes",
+                        "type": "button",
+                    },
+                    {
+                        "name":"no",
+                        "text": "No",
+                        "value": "no",
+                        "type": "button",
+                    }
+                ]
+            }
+        ]
+    });
+});
+
+
+controller.hears('^stop','direct_message',function(bot,message) {
+  bot.reply(message,'Goodbye');
+  bot.rtm.close();
+});
+
+controller.on(['direct_message','mention','direct_mention'],function(bot,message) {
+  bot.api.reactions.add({
+    timestamp: message.ts,
+    channel: message.channel,
+    name: 'robot_face',
+  },function(err) {
+    if (err) { console.log(err) }
+    bot.reply(message,'I heard you loud and clear boss.');
+  });
+});
+
+controller.storage.teams.all(function(err,teams) {
+
+  if (err) {
+    throw new Error(err);
+  }
+
+  // connect all teams with bots up to slack!
+  for (var t  in teams) {
+    if (teams[t].bot) {
+      controller.spawn(teams[t]).startRTM(function(err, bot) {
+        if (err) {
+          console.log('Error connecting bot to Slack:',err);
+        } else {
+          trackBot(bot);
+        }
+      });
+    }
+  }
+
+});
diff --git a/examples/slackbutton_incomingwebhooks.js b/examples/slackbutton_incomingwebhooks.js
old mode 100755
new mode 100644
diff --git a/lib/CoreBot.js b/lib/CoreBot.js
index ae12bb118..66cf03274 100755
--- a/lib/CoreBot.js
+++ b/lib/CoreBot.js
@@ -33,6 +33,7 @@ function Botkit(configuration) {
     botkit.middleware = {
         send: ware(),
         receive: ware(),
+        spawn: ware(),
     };
 
 
@@ -685,29 +686,6 @@ function Botkit(configuration) {
     };
 
 
-
-
-    botkit.debug = function() {
-        if (configuration.debug) {
-            var args = [];
-            for (var k = 0; k < arguments.length; k++) {
-                args.push(arguments[k]);
-            }
-            console.log.apply(null, args);
-        }
-    };
-
-    botkit.log = function() {
-        if (configuration.log || configuration.log === undefined) { //default to true
-            var args = [];
-            for (var k = 0; k < arguments.length; k++) {
-                args.push(arguments[k]);
-            }
-            console.log.apply(null, args);
-        }
-    };
-
-
     /**
      * hears_regexp - default string matcher uses regular expressions
      *
@@ -830,7 +808,12 @@ function Botkit(configuration) {
             });
         };
 
-        if (cb) { cb(worker); }
+        botkit.middleware.spawn.run(worker, function(err, worker, message) {
+
+            if (cb) { cb(worker); }
+
+        });
+
         return worker;
     };
 
diff --git a/lib/Facebook.js b/lib/Facebook.js
index bb453d09c..723be9d1e 100644
--- a/lib/Facebook.js
+++ b/lib/Facebook.js
@@ -113,8 +113,8 @@ function Facebookbot(configuration) {
     facebook_botkit.createWebhookEndpoints = function(webserver, bot, cb) {
 
         facebook_botkit.log(
-            '** Serving webhook endpoints for Slash commands and outgoing ' +
-            'webhooks at: http://MY_HOST:' + facebook_botkit.config.port + '/facebook/receive');
+            '** Serving webhook endpoints for Messenger Platform at: ' +
+            'http://MY_HOST:' + facebook_botkit.config.port + '/facebook/receive');
         webserver.post('/facebook/receive', function(req, res) {
 
             facebook_botkit.debug('GOT A MESSAGE HOOK');
diff --git a/lib/SlackBot.js b/lib/SlackBot.js
index b9ff31e40..2112676ae 100755
--- a/lib/SlackBot.js
+++ b/lib/SlackBot.js
@@ -8,10 +8,45 @@ function Slackbot(configuration) {
     // Create a core botkit bot
     var slack_botkit = Botkit(configuration || {});
 
+    var spawned_bots = [];
+
     // customize the bot definition, which will be used when new connections
     // spawn!
     slack_botkit.defineBot(require(__dirname + '/Slackbot_worker.js'));
 
+    // Middleware to track spawned bots and connect existing RTM bots to incoming webhooks
+    slack_botkit.middleware.spawn.use(function(worker, next) {
+
+        // lets first check and make sure we don't already have a bot
+        // for this team! If we already have an RTM connection, copy it
+        // into the new bot so it can be used for replies.
+
+        var existing_bot = null;
+        if (worker.config.id) {
+            for (var b = 0; b < spawned_bots.length; b++) {
+                if (spawned_bots[b].config.id) {
+                    if (spawned_bots[b].config.id == worker.config.id) {
+                        // WAIT! We already have a bot spawned here.
+                        // so instead of using the new one, use the exist one.
+                        existing_bot = spawned_bots[b];
+                    }
+                }
+            }
+        }
+
+        if (!existing_bot && worker.config.id) {
+            spawned_bots.push(worker);
+        } else {
+            if (existing_bot.rtm) {
+                worker.rtm = existing_bot.rtm;
+            }
+        }
+        next();
+
+    });
+
+
+
     // set up configuration for oauth
     // slack_app_config should contain
     // { clientId, clientSecret, scopes}
@@ -73,17 +108,57 @@ function Slackbot(configuration) {
 
     // set up a web route for receiving outgoing webhooks and/or slash commands
     slack_botkit.createWebhookEndpoints = function(webserver, authenticationTokens) {
+
+        if (authenticationTokens !== undefined && arguments.length > 1 && arguments[1].length) {
+            secureWebhookEndpoints.apply(null, arguments);
+        }
+
         slack_botkit.log(
             '** Serving webhook endpoints for Slash commands and outgoing ' +
             'webhooks at: http://MY_HOST:' + slack_botkit.config.port + '/slack/receive');
         webserver.post('/slack/receive', function(req, res) {
 
-            if (authenticationTokens !== undefined && arguments.length > 1 && arguments[1].length) {
-                secureWebhookEndpoints.apply(null, arguments);
-            }
+            // is this an interactive message callback?
+            if (req.body.payload) {
+
+                var message = JSON.parse(req.body.payload);
+                for (var key in req.body) {
+                    message[key] = req.body[key];
+                }
+
+                // let's normalize some of these fields to match the rtm message format
+                message.user = message.user.id;
+                message.channel = message.channel.id;
+
+                // put the action value in the text field
+                // this allows button clicks to respond to asks
+                message.text = message.actions[0].value;
+
+                message.type = 'interactive_message_callback';
+
+                slack_botkit.findTeamById(message.team.id, function(err, team) {
+                    if (err || !team) {
+                        slack_botkit.log.error('Received slash command, but could not load team');
+                    } else {
+                        res.status(200);
+                        res.send('');
+
+                        var bot = slack_botkit.spawn(team);
+
+                        bot.team_info = team;
+                        bot.res = res;
+
+                        slack_botkit.trigger('interactive_message_callback', [bot, message]);
+
+                        if (configuration.interactive_replies) {
+                            message.type = 'message';
+                            slack_botkit.receiveMessage(bot, message);
+                        }
+                    }
+                });
 
             // this is a slash command
-            if (req.body.command) {
+            } else if (req.body.command) {
                 var message = {};
 
                 for (var key in req.body) {
diff --git a/lib/Slackbot_worker.js b/lib/Slackbot_worker.js
index 28cee1b89..e404b1d5b 100755
--- a/lib/Slackbot_worker.js
+++ b/lib/Slackbot_worker.js
@@ -158,6 +158,7 @@ module.exports = function(botkit, config) {
             });
 
             bot.rtm.on('open', function() {
+                botkit.log.notice('RTM websocket opened');
 
                 pingIntervalId = setInterval(function() {
                     if (lastPong && lastPong + 12000 < Date.now()) {
@@ -204,11 +205,21 @@ module.exports = function(botkit, config) {
                 botkit.trigger('rtm_close', [bot, err]);
             });
 
-            bot.rtm.on('close', function() {
+            bot.rtm.on('close', function(code, message) {
+                botkit.log.notice('RTM close event: ' + code + ' : ' + message);
                 if (pingIntervalId) {
                     clearInterval(pingIntervalId);
                 }
                 botkit.trigger('rtm_close', [bot]);
+
+                /**
+                 * CLOSE_ABNORMAL error
+                 * wasn't closed explicitly, should attempt to reconnect
+                 */
+                if (code === 1006) {
+                    botkit.log.error('Abnormal websocket close event, attempting to reconnect');
+                    reconnect();
+                }
             });
         });
 
@@ -460,6 +471,39 @@ module.exports = function(botkit, config) {
         }
     };
 
+    bot.replyInteractive = function(src, resp, cb) {
+        if (!src.response_url) {
+            cb && cb('No response_url found');
+        } else {
+            var msg = {};
+
+            if (typeof(resp) == 'string') {
+                msg.text = resp;
+            } else {
+                msg = resp;
+            }
+
+            msg.channel = src.channel;
+
+            var requestOptions = {
+                uri: src.response_url,
+                method: 'POST',
+                json: msg
+            };
+            request(requestOptions, function(err, resp, body) {
+                /**
+                 * Do something?
+                 */
+                if (err) {
+                    botkit.log.error('Error sending interactive message response:', err);
+                    cb && cb(err);
+                } else {
+                    cb && cb();
+                }
+            });
+        }
+    };
+
     bot.reply = function(src, resp, cb) {
         var msg = {};
 
diff --git a/package.json b/package.json
index ca5c11bf3..01e1f398f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "botkit",
-  "version": "0.2.0",
+  "version": "0.2.2",
   "description": "Building blocks for Building Bots",
   "main": "lib/Botkit.js",
   "dependencies": {
diff --git a/readme-slack.md b/readme-slack.md
index 9a6125f7f..5c51cd57e 100644
--- a/readme-slack.md
+++ b/readme-slack.md
@@ -17,6 +17,7 @@ Table of Contents
 * [Slack-specific Events](#slack-specific-events)
 * [Working with Slack Custom Integrations](#working-with-slack-integrations)
 * [Using the Slack Button](#use-the-slack-button)
+* [Message Buttons](#message-buttons)
 
 ---
 ## Getting Started
@@ -595,3 +596,168 @@ bot.identifyBot(function(err,identity) {
 | create_user |
 | update_user |
 | oauth_error |
+
+
+## Message Buttons
+
+Slack applications can use "message buttons" or "interactive messages" to include buttons inside attachments. [Read the official Slack documentation here](https://api.slack.com/docs/message-buttons)
+
+Interactive messages can be sent via any of Botkit's built in functions by passing in
+the appropriate attachment as part of the message. When users click the buttons in Slack,
+Botkit triggers an `interactive_message_callback` event.
+
+When an `interactive_message_callback` is received, your bot can either reply with a new message, or use the special `bot.replyInteractive` function which will result in the original message in Slack being _replaced_ by the reply. Using `replyInteractive`, bots can present dynamic interfaces inside a single message.
+
+In order to use interactive messages, your bot will have to be [registered as a Slack application](https://api.slack.com/apps), and will have to use the Slack button authentication system.
+To receive callbacks, register a callback url as part of applications configuration. Botkit's built in support for the Slack Button system supports interactive message callbacks at the url `https://_your_server_/slack/receive` Note that Slack requires this url to be secured with https.
+
+During development, a tool such as [localtunnel.me](http://localtunnel.me) is useful for temporarily exposing a compatible webhook url to Slack while running Botkit privately.
+
+```
+// set up a botkit app to expose oauth and webhook endpoints
+controller.setupWebserver(process.env.port,function(err,webserver) {
+
+  // set up web endpoints for oauth, receiving webhooks, etc.
+  controller
+    .createHomepageEndpoint(controller.webserver)
+    .createOauthEndpoints(controller.webserver,function(err,req,res) { ... })
+    .createWebhookEndpoints(controller.webserver);
+
+});
+```
+
+### Send an interactive message
+```
+controller.hears('interactive', 'direct_message', function(bot, message) {
+
+    bot.reply(message, {
+        attachments:[
+            {
+                title: 'Do you want to interact with my buttons?',
+                callback_id: '123',
+                attachment_type: 'default',
+                actions: [
+                    {
+                        "name":"yes",
+                        "text": "Yes",
+                        "value": "yes",
+                        "type": "button",
+                    },
+                    {
+                        "name":"no",
+                        "text": "No",
+                        "value": "no",
+                        "type": "button",
+                    }
+                ]
+            }
+        ]
+    });    
+});
+```
+
+### Receive an interactive message callback
+
+```
+// receive an interactive message, and reply with a message that will replace the original
+controller.on('interactive_message_callback', function(bot, message) {
+
+    // check message.actions and message.callback_id to see what action to take...
+
+    bot.replyInteractive(message, {
+        text: '...',
+        attachments: [
+            {
+                title: 'My buttons',
+                callback_id: '123',
+                attachment_type: 'default',
+                actions: [
+                    {
+                        "name":"yes",
+                        "text": "Yes!",
+                        "value": "yes",
+                        "type": "button",
+                    },
+                    {
+                       "text": "No!",
+                        "name": "no",
+                        "value": "delete",
+                        "style": "danger",
+                        "type": "button",
+                        "confirm": {
+                          "title": "Are you sure?",
+                          "text": "This will do something!",
+                          "ok_text": "Yes",
+                          "dismiss_text": "No"
+                        }
+                    }
+                ]
+            }
+        ]
+    });
+
+});
+```
+
+### Using Interactive Messages in Conversations
+
+It is possible to use interactive messages in conversations, with the `convo.ask` function.
+In order to do this, you must instantiate your Botkit controller with the `interactive_replies` option set to `true`:
+
+```
+var controller = Botkit.slackbot({interactive_replies: true});
+```
+
+This will cause Botkit to pass all interactive_message_callback messages into the normal conversation
+system. When used in conjunction with `convo.ask`, expect the response text to match the button `value` field.
+
+```
+bot.startConversation(message, function(err, convo) {
+
+    convo.ask({
+        attachments:[
+            {
+                title: 'Do you want to proceed?',
+                callback_id: '123',
+                attachment_type: 'default',
+                actions: [
+                    {
+                        "name":"yes",
+                        "text": "Yes",
+                        "value": "yes",
+                        "type": "button",
+                    },
+                    {
+                        "name":"no",
+                        "text": "No",
+                        "value": "no",
+                        "type": "button",
+                    }
+                ]
+            }
+        ]
+    },[
+        {
+            pattern: "yes",
+            callback: function(reply, convo) {
+                convo.say('FABULOUS!');
+                convo.next();
+                // do something awesome here.
+            }
+        },
+        {
+            pattern: "no",
+            callback: function(reply, convo) {
+                convo.say('Too bad');
+                convo.next();
+            }
+        },
+        {   
+            default: true,
+            callback: function(reply, convo) {
+                // do nothing
+            }
+        }
+    ]);
+});
+```
diff --git a/readme.md b/readme.md
index a8bd72d42..b3c493ec6 100755
--- a/readme.md
+++ b/readme.md
@@ -108,7 +108,7 @@ This sample bot listens for the word "hello" to be said to it -- either as a dir
 The Botkit constructor returns a `controller` object. By attaching event handlers
 to the controller object, developers can specify what their bot should look for and respond to,
 including keywords, patterns and various [messaging and status events](#responding-to-events).
-These event handlers can be thought of metaphorically as skills or features the robot brain has -- each event handler defines a new "When a human say THIS the bot does THAT."
+These event handlers can be thought of metaphorically as skills or features the robot brain has -- each event handler defines a new "When a human says THIS the bot does THAT."
 
 The `controller` object is then used to `spawn()` bot instances that represent
 a specific bot identity and connection to Slack. Once spawned and connected to
diff --git a/slack_bot.js b/slack_bot.js
index d71da52d2..09963a1ba 100644
--- a/slack_bot.js
+++ b/slack_bot.js
@@ -73,7 +73,7 @@ var Botkit = require('./lib/Botkit.js');
 var os = require('os');
 
 var controller = Botkit.slackbot({
-    debug: true,
+    debug: true
 });
 
 var bot = controller.spawn({