diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js
index 50e7af645..1d05a8c7a 100644
--- a/src/blocks/scratch3_control.js
+++ b/src/blocks/scratch3_control.js
@@ -15,14 +15,16 @@ Scratch3ControlBlocks.prototype.getPrimitives = function() {
         'control_repeat': this.repeat,
         'control_forever': this.forever,
         'control_wait': this.wait,
+        'control_if': this.if,
+        'control_if_else': this.ifElse,
         'control_stop': this.stop
     };
 };
 
-Scratch3ControlBlocks.prototype.repeat = function(argValues, util) {
+Scratch3ControlBlocks.prototype.repeat = function(args, util) {
     // Initialize loop
     if (util.stackFrame.loopCounter === undefined) {
-        util.stackFrame.loopCounter = parseInt(argValues[0]); // @todo arg
+        util.stackFrame.loopCounter = parseInt(args.TIMES);
     }
     // Decrease counter
     util.stackFrame.loopCounter--;
@@ -32,15 +34,39 @@ Scratch3ControlBlocks.prototype.repeat = function(argValues, util) {
     }
 };
 
-Scratch3ControlBlocks.prototype.forever = function(argValues, util) {
+Scratch3ControlBlocks.prototype.forever = function(args, util) {
     util.startSubstack();
 };
 
-Scratch3ControlBlocks.prototype.wait = function(argValues, util) {
+Scratch3ControlBlocks.prototype.wait = function(args, util) {
     util.yield();
     util.timeout(function() {
         util.done();
-    }, 1000 * parseFloat(argValues[0]));
+    }, 1000 * args.DURATION);
+};
+
+Scratch3ControlBlocks.prototype.if = function(args, util) {
+    // Only execute one time. `if` will be returned to
+    // when the substack finishes, but it shouldn't execute again.
+    if (util.stackFrame.executed === undefined) {
+        util.stackFrame.executed = true;
+        if (args.CONDITION) {
+            util.startSubstack();
+        }
+    }
+};
+
+Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
+    // Only execute one time. `ifElse` will be returned to
+    // when the substack finishes, but it shouldn't execute again.
+    if (util.stackFrame.executed === undefined) {
+        util.stackFrame.executed = true;
+        if (args.CONDITION) {
+            util.startSubstack(1);
+        } else {
+            util.startSubstack(2);
+        }
+    }
 };
 
 Scratch3ControlBlocks.prototype.stop = function() {
diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js
index ff15416df..18a5ab621 100644
--- a/src/blocks/scratch3_event.js
+++ b/src/blocks/scratch3_event.js
@@ -27,17 +27,8 @@ Scratch3EventBlocks.prototype.whenBroadcastReceived = function() {
     // No-op
 };
 
-Scratch3EventBlocks.prototype.broadcast = function(argValues, util) {
-    util.startHats(function(hat) {
-        if (hat.opcode === 'event_whenbroadcastreceived') {
-            var shadows = hat.fields.CHOICE.blocks;
-            for (var sb in shadows) {
-                var shadowblock = shadows[sb];
-                return shadowblock.fields.CHOICE.value === argValues[0];
-            }
-        }
-        return false;
-    });
+Scratch3EventBlocks.prototype.broadcast = function() {
+    // @todo
 };
 
 module.exports = Scratch3EventBlocks;
diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js
new file mode 100644
index 000000000..eff1fe18d
--- /dev/null
+++ b/src/blocks/scratch3_operators.js
@@ -0,0 +1,38 @@
+function Scratch3OperatorsBlocks(runtime) {
+    /**
+     * The runtime instantiating this block package.
+     * @type {Runtime}
+     */
+    this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.<string, Function>} Mapping of opcode to Function.
+ */
+Scratch3OperatorsBlocks.prototype.getPrimitives = function() {
+    return {
+        'math_number': this.number,
+        'text': this.text,
+        'math_add': this.add,
+        'logic_equals': this.equals
+    };
+};
+
+Scratch3OperatorsBlocks.prototype.number = function (args) {
+    return Number(args.NUM);
+};
+
+Scratch3OperatorsBlocks.prototype.text = function (args) {
+    return String(args.TEXT);
+};
+
+Scratch3OperatorsBlocks.prototype.add = function (args) {
+    return args.NUM1 + args.NUM2;
+};
+
+Scratch3OperatorsBlocks.prototype.equals = function (args) {
+    return args.VALUE1 == args.VALUE2;
+};
+
+module.exports = Scratch3OperatorsBlocks;
diff --git a/src/engine/blocks.js b/src/engine/blocks.js
index 752cbb4d3..5ee0a7e10 100644
--- a/src/engine/blocks.js
+++ b/src/engine/blocks.js
@@ -22,6 +22,13 @@ function Blocks () {
     this._stacks = [];
 }
 
+/**
+ * Blockly inputs that represent statements/substacks
+ * are prefixed with this string.
+ * @const{string}
+ */
+Blocks.SUBSTACK_INPUT_PREFIX = 'SUBSTACK';
+
 /**
  * Provide an object with metadata for the requested block ID.
  * @param {!string} blockId ID of block we have stored.
@@ -60,7 +67,7 @@ Blocks.prototype.getSubstack = function (id, substackNum) {
     if (typeof block === 'undefined') return null;
     if (!substackNum) substackNum = 1;
 
-    var inputName = 'SUBSTACK';
+    var inputName = Blocks.SUBSTACK_INPUT_PREFIX;
     if (substackNum > 1) {
         inputName += substackNum;
     }
@@ -80,6 +87,34 @@ Blocks.prototype.getOpcode = function (id) {
     return this._blocks[id].opcode;
 };
 
+/**
+ * Get all fields and their values for a block.
+ * @param {?string} id ID of block to query.
+ * @return {!Object} All fields and their values.
+ */
+Blocks.prototype.getFields = function (id) {
+    if (typeof this._blocks[id] === 'undefined') return null;
+    return this._blocks[id].fields;
+};
+
+/**
+ * Get all non-substack inputs for a block.
+ * @param {?string} id ID of block to query.
+ * @return {!Object} All non-substack inputs and their associated blocks.
+ */
+Blocks.prototype.getInputs = function (id) {
+    if (typeof this._blocks[id] === 'undefined') return null;
+    var inputs = {};
+    for (var input in this._blocks[id].inputs) {
+        // Ignore blocks prefixed with substack prefix.
+        if (input.substring(0, Blocks.SUBSTACK_INPUT_PREFIX.length)
+            != Blocks.SUBSTACK_INPUT_PREFIX) {
+            inputs[input] = this._blocks[id].inputs[input];
+        }
+    }
+    return inputs;
+};
+
 // ---------------------------------------------------------------------
 
 /**
diff --git a/src/engine/execute.js b/src/engine/execute.js
new file mode 100644
index 000000000..d2f8381f0
--- /dev/null
+++ b/src/engine/execute.js
@@ -0,0 +1,94 @@
+var YieldTimers = require('../util/yieldtimers.js');
+
+/**
+ * If set, block calls, args, and return values will be logged to the console.
+ * @const {boolean}
+ */
+var DEBUG_BLOCK_CALLS = true;
+
+var execute = function (sequencer, thread) {
+    var runtime = sequencer.runtime;
+
+    // Current block to execute is the one on the top of the stack.
+    var currentBlockId = thread.peekStack();
+    var currentStackFrame = thread.peekStackFrame();
+
+    // Save the yield timer ID, in case a primitive makes a new one
+    // @todo hack - perhaps patch this to allow more than one timer per
+    // primitive, for example...
+    var oldYieldTimerId = YieldTimers.timerId;
+
+    var opcode = runtime.blocks.getOpcode(currentBlockId);
+
+    // Generate values for arguments (inputs).
+    var argValues = {};
+
+    // Add all fields on this block to the argValues.
+    var fields = runtime.blocks.getFields(currentBlockId);
+    for (var fieldName in fields) {
+        argValues[fieldName] = fields[fieldName].value;
+    }
+
+    // Recursively evaluate input blocks.
+    var inputs = runtime.blocks.getInputs(currentBlockId);
+    for (var inputName in inputs) {
+        var input = inputs[inputName];
+        var inputBlockId = input.block;
+        // Push to the stack to evaluate this input.
+        thread.pushStack(inputBlockId);
+        var result = execute(sequencer, thread);
+        thread.popStack();
+        argValues[input.name] = result;
+    }
+
+    if (!opcode) {
+        console.warn('Could not get opcode for block: ' + currentBlockId);
+        return;
+    }
+
+    var blockFunction = runtime.getOpcodeFunction(opcode);
+    if (!blockFunction) {
+        console.warn('Could not get implementation for opcode: ' + opcode);
+        return;
+    }
+
+    if (DEBUG_BLOCK_CALLS) {
+        console.groupCollapsed('Executing: ' + opcode);
+        console.log('with arguments: ', argValues);
+        console.log('and stack frame: ', currentStackFrame);
+    }
+    var primitiveReturnValue = null;
+    try {
+        // @todo deal with the return value
+        primitiveReturnValue = blockFunction(argValues, {
+            yield: thread.yield.bind(thread),
+            done: function() {
+                sequencer.proceedThread(thread);
+            },
+            timeout: YieldTimers.timeout,
+            stackFrame: currentStackFrame,
+            startSubstack: function (substackNum) {
+                sequencer.stepToSubstack(thread, substackNum);
+            }
+        });
+    }
+    catch(e) {
+        console.error(
+            'Exception calling block function for opcode: ' +
+            opcode + '\n' + e);
+    } finally {
+        // Update if the thread has set a yield timer ID
+        // @todo hack
+        if (YieldTimers.timerId > oldYieldTimerId) {
+            thread.yieldTimerId = YieldTimers.timerId;
+        }
+        if (DEBUG_BLOCK_CALLS) {
+            console.log('ending stack frame: ', currentStackFrame);
+            console.log('returned: ', primitiveReturnValue);
+            console.groupEnd();
+        }
+        return primitiveReturnValue;
+    }
+};
+
+module.exports = execute;
diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 5b4fab48d..aff2a3aec 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -6,6 +6,7 @@ var util = require('util');
 var defaultBlockPackages = {
     'scratch3_control': require('../blocks/scratch3_control'),
     'scratch3_event': require('../blocks/scratch3_event'),
+    'scratch3_operators': require('../blocks/scratch3_operators'),
     'wedo2': require('../blocks/wedo2')
 };
 
@@ -121,6 +122,7 @@ Runtime.prototype.getOpcodeFunction = function (opcode) {
 Runtime.prototype._pushThread = function (id) {
     this.emit(Runtime.STACK_GLOW_ON, id);
     var thread = new Thread(id);
+    thread.pushStack(id);
     this.threads.push(thread);
 };
 
@@ -231,6 +233,9 @@ Runtime.prototype._step = function () {
  * @param {boolean} isGlowing True to turn on glow; false to turn off.
  */
 Runtime.prototype.glowBlock = function (blockId, isGlowing) {
+    if (!this.blocks.getBlock(blockId)) {
+        return;
+    }
     if (isGlowing) {
         this.emit(Runtime.BLOCK_GLOW_ON, blockId);
     } else {
diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js
index c31081798..deaa15266 100644
--- a/src/engine/sequencer.js
+++ b/src/engine/sequencer.js
@@ -1,6 +1,7 @@
 var Timer = require('../util/timer');
 var Thread = require('./thread');
 var YieldTimers = require('../util/yieldtimers.js');
+var execute = require('./execute.js');
 
 function Sequencer (runtime) {
     /**
@@ -24,12 +25,6 @@ function Sequencer (runtime) {
  */
 Sequencer.WORK_TIME = 10;
 
-/**
- * If set, block calls, args, and return values will be logged to the console.
- * @const {boolean}
- */
-Sequencer.DEBUG_BLOCK_CALLS = true;
-
 /**
  * Step through all threads in `this.threads`, running them in order.
  * @param {Array.<Thread>} threads List of which threads to step.
@@ -49,33 +44,27 @@ Sequencer.prototype.stepThreads = function (threads) {
            this.timer.timeElapsed() < Sequencer.WORK_TIME) {
         // New threads at the end of the iteration.
         var newThreads = [];
+        // Reset yielding thread count.
+        numYieldingThreads = 0;
         // Attempt to run each thread one time
         for (var i = 0; i < threads.length; i++) {
             var activeThread = threads[i];
             if (activeThread.status === Thread.STATUS_RUNNING) {
                 // Normal-mode thread: step.
-                this.stepThread(activeThread);
+                this.startThread(activeThread);
             } else if (activeThread.status === Thread.STATUS_YIELD) {
                 // Yield-mode thread: check if the time has passed.
-                YieldTimers.resolve(activeThread.yieldTimerId);
-                numYieldingThreads++;
+                if (!YieldTimers.resolve(activeThread.yieldTimerId)) {
+                    // Thread is still yielding
+                    // if YieldTimers.resolve returns false.
+                    numYieldingThreads++;
+                }
             } else if (activeThread.status === Thread.STATUS_DONE) {
                 // Moved to a done state - finish up
                 activeThread.status = Thread.STATUS_RUNNING;
                 // @todo Deal with the return value
             }
-            // First attempt to pop from the stack
-            if (activeThread.stack.length > 0 &&
-                activeThread.nextBlock === null &&
-                activeThread.status === Thread.STATUS_DONE) {
-                activeThread.nextBlock = activeThread.stack.pop();
-                // Don't pop stack frame - we need the data.
-                // A new one won't be created when we execute.
-                if (activeThread.nextBlock !== null) {
-                    activeThread.status === Thread.STATUS_RUNNING;
-                }
-            }
-            if (activeThread.nextBlock === null &&
+            if (activeThread.stack.length === 0 &&
                 activeThread.status === Thread.STATUS_DONE) {
                 // Finished with this thread - tell runtime to clean it up.
                 inactiveThreads.push(activeThread);
@@ -94,175 +83,71 @@ Sequencer.prototype.stepThreads = function (threads) {
  * Step the requested thread
  * @param {!Thread} thread Thread object to step
  */
-Sequencer.prototype.stepThread = function (thread) {
-    // Save the yield timer ID, in case a primitive makes a new one
-    // @todo hack - perhaps patch this to allow more than one timer per
-    // primitive, for example...
-    var oldYieldTimerId = YieldTimers.timerId;
-
-    // Save the current block and set the nextBlock.
-    // If the primitive would like to do control flow,
-    // it can overwrite nextBlock.
-    var currentBlock = thread.nextBlock;
-    if (!currentBlock || !this.runtime.blocks.getBlock(currentBlock)) {
+Sequencer.prototype.startThread = function (thread) {
+    var currentBlockId = thread.peekStack();
+    if (!currentBlockId) {
+        // A "null block" - empty substack. Pop the stack.
+        thread.popStack();
         thread.status = Thread.STATUS_DONE;
         return;
     }
-    thread.nextBlock = this.runtime.blocks.getNextBlock(currentBlock);
-
-    var opcode = this.runtime.blocks.getOpcode(currentBlock);
-
-    // Push the current block to the stack
-    thread.stack.push(currentBlock);
-    // Push an empty stack frame, if we need one.
-    // Might not, if we just popped the stack.
-    if (thread.stack.length > thread.stackFrames.length) {
-        thread.stackFrames.push({});
-    }
-    var currentStackFrame = thread.stackFrames[thread.stackFrames.length - 1];
-
-    /**
-     * A callback for the primitive to indicate its thread should yield.
-     * @type {Function}
-     */
-    var threadYieldCallback = function () {
-        thread.status = Thread.STATUS_YIELD;
-    };
-
-    /**
-     * A callback for the primitive to indicate its thread is finished
-     * @type {Function}
-     */
-    var instance = this;
-    var threadDoneCallback = function () {
-        thread.status = Thread.STATUS_DONE;
-        // Refresh nextBlock in case it has changed during a yield.
-        thread.nextBlock = instance.runtime.blocks.getNextBlock(currentBlock);
-        // Pop the stack and stack frame
-        thread.stack.pop();
-        thread.stackFrames.pop();
-        // Stop showing run feedback in the editor.
-        instance.runtime.glowBlock(currentBlock, false);
-    };
-
-    /**
-     * A callback for the primitive to start hats.
-     * @todo very hacked...
-     * Provide a callback that is passed in a block and returns true
-     * if it is a hat that should be triggered.
-     * @param {Function} callback Provided callback.
-     */
-    var startHats = function(callback) {
-        var stacks = instance.runtime.blocks.getStacks();
-        for (var i = 0; i < stacks.length; i++) {
-            var stack = stacks[i];
-            var stackBlock = instance.runtime.blocks.getBlock(stack);
-            var result = callback(stackBlock);
-            if (result) {
-                // Check if the stack is already running
-                var stackRunning = false;
-
-                for (var j = 0; j < instance.runtime.threads.length; j++) {
-                    if (instance.runtime.threads[j].topBlock == stack) {
-                        stackRunning = true;
-                        break;
-                    }
-                }
-                if (!stackRunning) {
-                    instance.runtime._pushThread(stack);
-                }
-            }
-        }
-    };
-
-    /**
-     * Record whether we have switched stack,
-     * to avoid proceeding the thread automatically.
-     * @type {boolean}
-     */
-    var switchedStack = false;
-    /**
-     * A callback for a primitive to start a substack.
-     * @type {Function}
-     */
-    var threadStartSubstack = function () {
-        // Set nextBlock to the start of the substack
-        var substack = instance.runtime.blocks.getSubstack(currentBlock);
-        if (substack && substack.value) {
-            thread.nextBlock = substack.value;
-        } else {
-            thread.nextBlock = null;
-        }
-        switchedStack = true;
-    };
-
-    // @todo extreme hack to get the single argument value for prototype
-    var argValues = [];
-    var blockInputs = this.runtime.blocks.getBlock(currentBlock).fields;
-    for (var bi in blockInputs) {
-        var outer = blockInputs[bi];
-        for (var b in outer.blocks) {
-            var block = outer.blocks[b];
-            var fields = block.fields;
-            for (var f in fields) {
-                var field = fields[f];
-                argValues.push(field.value);
-            }
-        }
-    }
-
     // Start showing run feedback in the editor.
-    this.runtime.glowBlock(currentBlock, true);
+    this.runtime.glowBlock(currentBlockId, true);
 
-    if (!opcode) {
-        console.warn('Could not get opcode for block: ' + currentBlock);
-    }
-    else {
-        var blockFunction = this.runtime.getOpcodeFunction(opcode);
-        if (!blockFunction) {
-            console.warn('Could not get implementation for opcode: ' + opcode);
-        }
-        else {
-            if (Sequencer.DEBUG_BLOCK_CALLS) {
-                console.groupCollapsed('Executing: ' + opcode);
-                console.log('with arguments: ', argValues);
-                console.log('and stack frame: ', currentStackFrame);
-            }
-            var blockFunctionReturnValue = null;
-            try {
-                // @todo deal with the return value
-                blockFunctionReturnValue = blockFunction(argValues, {
-                    yield: threadYieldCallback,
-                    done: threadDoneCallback,
-                    timeout: YieldTimers.timeout,
-                    stackFrame: currentStackFrame,
-                    startSubstack: threadStartSubstack,
-                    startHats: startHats
-                });
-            }
-            catch(e) {
-                console.error(
-                    'Exception calling block function for opcode: ' +
-                    opcode + '\n' + e);
-            } finally {
-                // Update if the thread has set a yield timer ID
-                // @todo hack
-                if (YieldTimers.timerId > oldYieldTimerId) {
-                    thread.yieldTimerId = YieldTimers.timerId;
-                }
-                if (thread.status === Thread.STATUS_RUNNING && !switchedStack) {
-                    // Thread executed without yielding - move to done
-                    threadDoneCallback();
-                }
-                if (Sequencer.DEBUG_BLOCK_CALLS) {
-                    console.log('ending stack frame: ', currentStackFrame);
-                    console.log('returned: ', blockFunctionReturnValue);
-                    console.groupEnd();
-                }
-            }
-        }
-    }
+    // Execute the current block
+    execute(this, thread);
 
+    // If the block executed without yielding and without doing control flow,
+    // move to done.
+    if (thread.status === Thread.STATUS_RUNNING &&
+        thread.peekStack() === currentBlockId) {
+        this.proceedThread(thread, currentBlockId);
+    }
+};
+
+/**
+ * Step a thread into a block's substack.
+ * @param {!Thread} thread Thread object to step to substack.
+ * @param {Number} substackNum Which substack to step to (i.e., 1, 2).
+ */
+Sequencer.prototype.stepToSubstack = function (thread, substackNum) {
+    if (!substackNum) {
+        substackNum = 1;
+    }
+    var currentBlockId = thread.peekStack();
+    var substackId = this.runtime.blocks.getSubstack(
+        currentBlockId,
+        substackNum
+    );
+    if (substackId) {
+        // Push substack ID to the thread's stack.
+        thread.pushStack(substackId);
+    } else {
+        // Push null, so we come back to the current block.
+        thread.pushStack(null);
+    }
+};
+
+/**
+ * Finish stepping a thread and proceed it to the next block.
+ * @param {!Thread} thread Thread object to proceed.
+ */
+Sequencer.prototype.proceedThread = function (thread) {
+    var currentBlockId = thread.peekStack();
+    // Mark the status as done and proceed to the next block.
+    this.runtime.glowBlock(currentBlockId, false);
+    thread.status = Thread.STATUS_DONE;
+    // Pop from the stack - finished this level of execution.
+    thread.popStack();
+    // Push next connected block, if there is one.
+    var nextBlockId = this.runtime.blocks.getNextBlock(currentBlockId);
+    if (nextBlockId) {
+        thread.pushStack(nextBlockId);
+    }
+    // Pop from the stack until we have a next block.
+    while (thread.peekStack() === null && thread.stack.length > 0) {
+        thread.popStack();
+    }
 };
 
 module.exports = Sequencer;
diff --git a/src/engine/thread.js b/src/engine/thread.js
index 07ceaca35..c98efab48 100644
--- a/src/engine/thread.js
+++ b/src/engine/thread.js
@@ -9,11 +9,7 @@ function Thread (firstBlock) {
      * @type {!string}
      */
     this.topBlock = firstBlock;
-    /**
-     * ID of next block that the thread will execute, or null if none.
-     * @type {?string}
-     */
-    this.nextBlock = firstBlock;
+
     /**
      * Stack for the thread. When the sequencer enters a control structure,
      * the block is pushed onto the stack so we know where to exit.
@@ -62,4 +58,50 @@ Thread.STATUS_YIELD = 1;
  */
 Thread.STATUS_DONE = 2;
 
+/**
+ * Push stack and update stack frames appropriately.
+ * @param {string} blockId Block ID to push to stack.
+ */
+Thread.prototype.pushStack = function (blockId) {
+    this.stack.push(blockId);
+    // Push an empty stack frame, if we need one.
+    // Might not, if we just popped the stack.
+    if (this.stack.length > this.stackFrames.length) {
+        this.stackFrames.push({});
+    }
+};
+
+/**
+ * Pop last block on the stack and its stack frame.
+ * @return {string} Block ID popped from the stack.
+ */
+Thread.prototype.popStack = function () {
+    this.stackFrames.pop();
+    return this.stack.pop();
+};
+
+/**
+ * Get top stack item.
+ * @return {?string} Block ID on top of stack.
+ */
+Thread.prototype.peekStack = function () {
+    return this.stack[this.stack.length - 1];
+};
+
+
+/**
+ * Get top stack frame.
+ * @return {?Object} Last stack frame stored on this thread.
+ */
+Thread.prototype.peekStackFrame = function () {
+    return this.stackFrames[this.stackFrames.length - 1];
+};
+
+/**
+ * Yields the thread.
+ */
+Thread.prototype.yield = function () {
+    this.status = Thread.STATUS_YIELD;
+};
+
 module.exports = Thread;