diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js
index 18a5ab621..71326d494 100644
--- a/src/blocks/scratch3_event.js
+++ b/src/blocks/scratch3_event.js
@@ -12,23 +12,72 @@ function Scratch3EventBlocks(runtime) {
  */
 Scratch3EventBlocks.prototype.getPrimitives = function() {
     return {
-        'event_whenflagclicked': this.whenFlagClicked,
-        'event_whenbroadcastreceived': this.whenBroadcastReceived,
-        'event_broadcast': this.broadcast
+        'event_broadcast': this.broadcast,
+        'event_broadcastandwait': this.broadcastAndWait,
+        'event_whengreaterthan': this.hatGreaterThanPredicate
     };
 };
 
-
-Scratch3EventBlocks.prototype.whenFlagClicked = function() {
-    // No-op
+Scratch3EventBlocks.prototype.getHats = function () {
+    return {
+        'event_whenflagclicked': {
+            restartExistingThreads: true
+        },
+        /*'event_whenkeypressed': {
+            restartExistingThreads: false
+        },
+        'event_whenthisspriteclicked': {
+            restartExistingThreads: true
+        },
+        'event_whenbackdropswitchesto': {
+            restartExistingThreads: true
+        },*/
+        'event_whengreaterthan': {
+            restartExistingThreads: false,
+            edgeActivated: true
+        },
+        'event_whenbroadcastreceived': {
+            restartExistingThreads: true
+        }
+    };
 };
 
-Scratch3EventBlocks.prototype.whenBroadcastReceived = function() {
-    // No-op
+Scratch3EventBlocks.prototype.hatGreaterThanPredicate = function (args, util) {
+    // @todo: Other cases :)
+    if (args.WHENGREATERTHANMENU == 'TIMER') {
+        return util.ioQuery('clock', 'projectTimer') > args.VALUE;
+    }
+    return false;
 };
 
-Scratch3EventBlocks.prototype.broadcast = function() {
-    // @todo
+Scratch3EventBlocks.prototype.broadcast = function(args, util) {
+    util.startHats('event_whenbroadcastreceived', {
+        'BROADCAST_OPTION': args.BROADCAST_OPTION
+    });
+};
+
+Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) {
+    // Have we run before, starting threads?
+    if (!util.stackFrame.startedThreads) {
+        // No - start hats for this broadcast.
+        util.stackFrame.startedThreads = util.startHats(
+            'event_whenbroadcastreceived', {
+                'BROADCAST_OPTION': args.BROADCAST_OPTION
+            }
+        );
+        if (util.stackFrame.startedThreads.length == 0) {
+            // Nothing was started.
+            return;
+        }
+    }
+    // We've run before; check if the wait is still going on.
+    var instance = this;
+    var waiting = util.stackFrame.startedThreads.some(function(thread) {
+        return instance.runtime.isActiveThread(thread);
+    });
+    if (waiting) {
+        util.yieldFrame();
+    }
 };
 
 module.exports = Scratch3EventBlocks;
diff --git a/src/engine/execute.js b/src/engine/execute.js
index d885a6c58..5efe9bebd 100644
--- a/src/engine/execute.js
+++ b/src/engine/execute.js
@@ -1,5 +1,14 @@
 var Thread = require('./thread');
 
+/**
+ * Utility function to determine if a value is a Promise.
+ * @param {*} value Value to check for a Promise.
+ * @return {Boolean} True if the value appears to be a Promise.
+ */
+var isPromise = function (value) {
+    return value && value.then && typeof value.then === 'function';
+};
+
 /**
  * Execute a block.
  * @param {!Sequencer} sequencer Which sequencer is executing.
@@ -21,8 +30,17 @@ var execute = function (sequencer, thread) {
     }
 
     var blockFunction = runtime.getOpcodeFunction(opcode);
+    var isHat = runtime.getIsHat(opcode);
+
+    // Hats are implemented slightly differently from regular blocks.
+    // If they have an associated block function, it's treated as a predicate;
+    // if not, execution will proceed right through it (as a no-op).
     if (!blockFunction) {
-        console.warn('Could not get implementation for opcode: ' + opcode);
+        if (!isHat) {
+            console.warn('Could not get implementation for opcode: ' + opcode);
+        }
+        // Skip through the block.
+        // (either hat with no predicate, or missing op).
         return;
     }
 
@@ -63,6 +81,8 @@ var execute = function (sequencer, thread) {
 
     var primitiveReportedValue = null;
     primitiveReportedValue = blockFunction(argValues, {
+        stackFrame: currentStackFrame.executionContext,
+        target: target,
         yield: function() {
             thread.setStatus(Thread.STATUS_YIELD);
         },
@@ -73,11 +93,14 @@ var execute = function (sequencer, thread) {
             thread.setStatus(Thread.STATUS_RUNNING);
             sequencer.proceedThread(thread);
         },
-        stackFrame: currentStackFrame.executionContext,
         startBranch: function (branchNum) {
             sequencer.stepToBranch(thread, branchNum);
         },
-        target: target,
+        startHats: function(requestedHat, opt_matchFields, opt_target) {
+            return (
+                runtime.startHats(requestedHat, opt_matchFields, opt_target)
+            );
+        },
         ioQuery: function (device, func, args) {
             // Find the I/O device and execute the query/function call.
             if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
@@ -87,28 +110,53 @@ var execute = function (sequencer, thread) {
         }
     });
 
-    // Deal with any reported value.
+    /**
+     * Handle any reported value from the primitive, either directly returned
+     * or after a promise resolves.
+     * @param {*} resolvedValue Value eventually returned from the primitive.
+     */
+    var handleReport = function (resolvedValue) {
+        thread.pushReportedValue(resolvedValue);
+        if (isHat) {
+            // Hat predicate was evaluated.
+            if (runtime.getIsEdgeActivatedHat(opcode)) {
+                // If this is an edge-activated hat, only proceed if
+                // the value is true and used to be false.
+                var oldEdgeValue = runtime.updateEdgeActivatedValue(
+                    currentBlockId,
+                    resolvedValue
+                );
+                var edgeWasActivated = !oldEdgeValue && resolvedValue;
+                if (!edgeWasActivated) {
+                    sequencer.retireThread(thread);
+                }
+            } else {
+                // Not an edge-activated hat: retire the thread
+                // if predicate was false.
+                if (!resolvedValue) {
+                    sequencer.retireThread(thread);
+                }
+            }
+        } else {
+            // In a non-hat, report the value visually if necessary if
+            // at the top of the thread stack.
+            if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
+                runtime.visualReport(currentBlockId, resolvedValue);
+            }
+            // Finished any yields.
+            thread.setStatus(Thread.STATUS_RUNNING);
+        }
+    };
+
     // If it's a promise, wait until promise resolves.
-    var isPromise = (
-        primitiveReportedValue &&
-        primitiveReportedValue.then &&
-        typeof primitiveReportedValue.then === 'function'
-    );
-    if (isPromise) {
+    if (isPromise(primitiveReportedValue)) {
         if (thread.status === Thread.STATUS_RUNNING) {
             // Primitive returned a promise; automatically yield thread.
             thread.setStatus(Thread.STATUS_YIELD);
         }
         // Promise handlers
         primitiveReportedValue.then(function(resolvedValue) {
-            // Promise resolved: the primitive reported a value.
-            thread.pushReportedValue(resolvedValue);
-            // Report the value visually if necessary.
-            if (typeof resolvedValue !== 'undefined' &&
-                thread.peekStack() === thread.topBlock) {
-                runtime.visualReport(thread.peekStack(), resolvedValue);
-            }
-            thread.setStatus(Thread.STATUS_RUNNING);
+            handleReport(resolvedValue);
             sequencer.proceedThread(thread);
         }, function(rejectionReason) {
             // Promise rejected: the primitive had some error.
@@ -118,12 +166,7 @@ var execute = function (sequencer, thread) {
             sequencer.proceedThread(thread);
         });
     } else if (thread.status === Thread.STATUS_RUNNING) {
-        thread.pushReportedValue(primitiveReportedValue);
-        // Report the value visually if necessary.
-        if (typeof primitiveReportedValue !== 'undefined' &&
-            thread.peekStack() === thread.topBlock) {
-            runtime.visualReport(thread.peekStack(), primitiveReportedValue);
-        }
+        handleReport(primitiveReportedValue);
     }
 };
 
diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index d244544aa..921839497 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -47,6 +47,8 @@ function Runtime (targets) {
      * @type {Object.<string, Function>}
      */
     this._primitives = {};
+    this._hats = {};
+    this._edgeActivatedHatValues = {};
     this._registerBlockPackages();
 
     this.ioDevices = {
@@ -109,11 +111,23 @@ Runtime.prototype._registerBlockPackages = function () {
         if (defaultBlockPackages.hasOwnProperty(packageName)) {
             // @todo pass a different runtime depending on package privilege?
             var packageObject = new (defaultBlockPackages[packageName])(this);
-            var packageContents = packageObject.getPrimitives();
-            for (var op in packageContents) {
-                if (packageContents.hasOwnProperty(op)) {
-                    this._primitives[op] =
-                        packageContents[op].bind(packageObject);
+            // Collect primitives from package.
+            if (packageObject.getPrimitives) {
+                var packagePrimitives = packageObject.getPrimitives();
+                for (var op in packagePrimitives) {
+                    if (packagePrimitives.hasOwnProperty(op)) {
+                        this._primitives[op] =
+                            packagePrimitives[op].bind(packageObject);
+                    }
+                }
+            }
+            // Collect hat metadata from package.
+            if (packageObject.getHats) {
+                var packageHats = packageObject.getHats();
+                for (var hatName in packageHats) {
+                    if (packageHats.hasOwnProperty(hatName)) {
+                        this._hats[hatName] = packageHats[hatName];
+                    }
                 }
             }
         }
@@ -132,15 +146,58 @@ Runtime.prototype.getOpcodeFunction = function (opcode) {
 // -----------------------------------------------------------------------------
 // -----------------------------------------------------------------------------
 
+/**
+ * Return whether an opcode represents a hat block.
+ * @param {!string} opcode The opcode to look up.
+ * @return {Boolean} True if the op is known to be a hat.
+ */
+Runtime.prototype.getIsHat = function (opcode) {
+    return this._hats.hasOwnProperty(opcode);
+};
+
+/**
+ * Return whether an opcode represents an edge-activated hat block.
+ * @param {!string} opcode The opcode to look up.
+ * @return {Boolean} True if the op is known to be a edge-activated hat.
+ */
+Runtime.prototype.getIsEdgeActivatedHat = function (opcode) {
+    return this._hats.hasOwnProperty(opcode) &&
+        this._hats[opcode].edgeActivated;
+};
+
+/**
+ * Update an edge-activated hat block value.
+ * @param {!string} blockId ID of hat to store value for.
+ * @param {*} newValue Value to store for edge-activated hat.
+ * @return {*} The old value for the edge-activated hat.
+ */
+Runtime.prototype.updateEdgeActivatedValue = function (blockId, newValue) {
+    var oldValue = this._edgeActivatedHatValues[blockId];
+    this._edgeActivatedHatValues[blockId] = newValue;
+    return oldValue;
+};
+
+/**
+ * Clear all edge-activaed hat values.
+ */
+Runtime.prototype.clearEdgeActivatedValues = function () {
+    this._edgeActivatedHatValues = {};
+};
+
+// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
+
 /**
  * Create a thread and push it to the list of threads.
  * @param {!string} id ID of block that starts the stack
+ * @return {!Thread} The newly created thread.
  */
 Runtime.prototype._pushThread = function (id) {
-    this.emit(Runtime.STACK_GLOW_ON, id);
     var thread = new Thread(id);
+    this.glowScript(id, true);
     thread.pushStack(id);
     this.threads.push(thread);
+    return thread;
 };
 
 /**
@@ -150,11 +207,20 @@ Runtime.prototype._pushThread = function (id) {
 Runtime.prototype._removeThread = function (thread) {
     var i = this.threads.indexOf(thread);
     if (i > -1) {
-        this.emit(Runtime.STACK_GLOW_OFF, thread.topBlock);
+        this.glowScript(thread.topBlock, false);
         this.threads.splice(i, 1);
     }
 };
 
+/**
+ * Return whether a thread is currently active/running.
+ * @param {?Thread} thread Thread object to check.
+ * @return {Boolean} True if the thread is active/running.
+ */
+Runtime.prototype.isActiveThread = function (thread) {
+    return this.threads.indexOf(thread) > -1;
+};
+
 /**
  * Toggle a script.
  * @param {!string} topBlockId ID of block that starts the script.
@@ -172,28 +238,100 @@ Runtime.prototype.toggleScript = function (topBlockId) {
 };
 
 /**
- * Green flag, which stops currently running threads
- * and adds all top-level scripts that start with the green flag
+ * Run a function `f` for all scripts in a workspace.
+ * `f` will be called with two parameters:
+ *  - the top block ID of the script.
+ *  - the target that owns the script.
+ * @param {!Function} f Function to call for each script.
+ * @param {Target=} opt_target Optionally, a target to restrict to.
  */
-Runtime.prototype.greenFlag = function () {
-    // Remove all existing threads
-    for (var i = 0; i < this.threads.length; i++) {
-        this._removeThread(this.threads[i]);
+Runtime.prototype.allScriptsDo = function (f, opt_target) {
+    var targets = this.targets;
+    if (opt_target) {
+        targets = [opt_target];
     }
-    // Add all top scripts with green flag
-    for (var t = 0; t < this.targets.length; t++) {
-        var target = this.targets[t];
+    for (var t = 0; t < targets.length; t++) {
+        var target = targets[t];
         var scripts = target.blocks.getScripts();
         for (var j = 0; j < scripts.length; j++) {
-            var topBlock = scripts[j];
-            if (target.blocks.getBlock(topBlock).opcode ===
-                'event_whenflagclicked') {
-                this._pushThread(scripts[j]);
-            }
+            var topBlockId = scripts[j];
+            f(topBlockId, target);
         }
     }
 };
 
+/**
+ * Start all relevant hats.
+ * @param {!string} requestedHatOpcode Opcode of hats to start.
+ * @param {Object=} opt_matchFields Optionally, fields to match on the hat.
+ * @param {Target=} opt_target Optionally, a target to restrict to.
+ * @return {Array.<Thread>} List of threads started by this function.
+ */
+Runtime.prototype.startHats = function (requestedHatOpcode,
+    opt_matchFields, opt_target) {
+    if (!this._hats.hasOwnProperty(requestedHatOpcode)) {
+        // No known hat with this opcode.
+        return;
+    }
+    var instance = this;
+    var newThreads = [];
+    // Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
+    this.allScriptsDo(function(topBlockId, target) {
+        var potentialHatOpcode = target.blocks.getBlock(topBlockId).opcode;
+        if (potentialHatOpcode !== requestedHatOpcode) {
+            // Not the right hat.
+            return;
+        }
+        // Match any requested fields.
+        // For example: ensures that broadcasts match.
+        // This needs to happen before the block is evaluated
+        // (i.e., before the predicate can be run) because "broadcast and wait"
+        // needs to have a precise collection of started threads.
+        var hatFields = target.blocks.getFields(topBlockId);
+        if (opt_matchFields) {
+            for (var matchField in opt_matchFields) {
+                if (hatFields[matchField].value !==
+                    opt_matchFields[matchField]) {
+                    // Field mismatch.
+                    return;
+                }
+            }
+        }
+        // Look up metadata for the relevant hat.
+        var hatMeta = instance._hats[requestedHatOpcode];
+        if (hatMeta.restartExistingThreads) {
+            // If `restartExistingThreads` is true, we should stop
+            // any existing threads starting with the top block.
+            for (var i = 0; i < instance.threads.length; i++) {
+                if (instance.threads[i].topBlock === topBlockId) {
+                    instance._removeThread(instance.threads[i]);
+                }
+            }
+        } else {
+            // If `restartExistingThreads` is false, we should
+            // give up if any threads with the top block are running.
+            for (var j = 0; j < instance.threads.length; j++) {
+                if (instance.threads[j].topBlock === topBlockId) {
+                    // Some thread is already running.
+                    return;
+                }
+            }
+        }
+        // Start the thread with this top block.
+        newThreads.push(instance._pushThread(topBlockId));
+    }, opt_target);
+    return newThreads;
+};
+
+/**
+ * Start all threads that start with the green flag.
+ */
+Runtime.prototype.greenFlag = function () {
+    this.ioDevices.clock.resetProjectTimer();
+    this.clearEdgeActivatedValues();
+    this.startHats('event_whenflagclicked');
+};
+
 /**
  * Stop "everything"
  */
@@ -215,6 +353,13 @@ Runtime.prototype.stopAll = function () {
  * inactive threads after each iteration.
  */
 Runtime.prototype._step = function () {
+    // Find all edge-activated hats, and add them to threads to be evaluated.
+    for (var hatType in this._hats) {
+        var hat = this._hats[hatType];
+        if (hat.edgeActivated) {
+            this.startHats(hatType);
+        }
+    }
     var inactiveThreads = this.sequencer.stepThreads(this.threads);
     for (var i = 0; i < inactiveThreads.length; i++) {
         this._removeThread(inactiveThreads[i]);
@@ -234,6 +379,19 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) {
     }
 };
 
+/**
+ * Emit feedback for script glowing.
+ * @param {?string} topBlockId ID for the top block to update glow
+ * @param {boolean} isGlowing True to turn on glow; false to turn off.
+ */
+Runtime.prototype.glowScript = function (topBlockId, isGlowing) {
+    if (isGlowing) {
+        this.emit(Runtime.STACK_GLOW_ON, topBlockId);
+    } else {
+        this.emit(Runtime.STACK_GLOW_OFF, topBlockId);
+    }
+};
+
 /**
  * Emit value for reporter to show in the blocks.
  * @param {string} blockId ID for the block.
diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js
index 73126a227..11b134b53 100644
--- a/src/engine/sequencer.js
+++ b/src/engine/sequencer.js
@@ -166,4 +166,14 @@ Sequencer.prototype.proceedThread = function (thread) {
     }
 };
 
+/**
+ * Retire a thread in the middle, without considering further blocks.
+ * @param {!Thread} thread Thread object to retire.
+ */
+Sequencer.prototype.retireThread = function (thread) {
+    thread.stack = [];
+    thread.stackFrame = [];
+    thread.setStatus(Thread.STATUS_DONE);
+};
+
 module.exports = Sequencer;
diff --git a/src/engine/thread.js b/src/engine/thread.js
index d1bd73fc0..07a98b862 100644
--- a/src/engine/thread.js
+++ b/src/engine/thread.js
@@ -123,6 +123,14 @@ Thread.prototype.pushReportedValue = function (value) {
     }
 };
 
+/**
+ * Whether the current execution of a thread is at the top of the stack.
+ * @return {Boolean} True if execution is at top of the stack.
+ */
+Thread.prototype.atStackTop = function () {
+    return this.peekStack() === this.topBlock;
+};
+
 /**
  * Set thread status.
  * @param {number} status Enum representing thread status.