diff --git a/package-lock.json b/package-lock.json
index 5c2b4e73c..e99a0c20d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -64,6 +64,7 @@
 				"@typescript/vfs": "^1.3.4",
 				"chai": "^4.3.6",
 				"classnames": "^2.3.1",
+				"console-feed": "^3.3.0",
 				"cross-env": "^7.0.3",
 				"es-module-shims": "1.4.3",
 				"fake-indexeddb": "^3.1.2",
@@ -72,6 +73,7 @@
 				"frontend-collective-react-dnd-scrollzone": "1.0.2",
 				"glob": "^7.2.0",
 				"lodash": "^4.17.21",
+				"logdown": "^3.3.1",
 				"lowlight": "^1.20.0",
 				"lz-string": "^1.4.4",
 				"markdown-it": "^12.0.2",
@@ -10974,6 +10976,31 @@
 			"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
 			"dev": true
 		},
+		"node_modules/console-feed": {
+			"version": "3.3.0",
+			"resolved": "https://registry.npmjs.org/console-feed/-/console-feed-3.3.0.tgz",
+			"integrity": "sha512-GS0EtpiLyAZGEYBtTih+uI3s3NDmOsfkgpNGhr7UWeM5BzDT+dKgit2nEMFwDb2w7NaT95774/cwAztA1BxrHQ==",
+			"dependencies": {
+				"@emotion/core": "^10.0.10",
+				"@emotion/styled": "^10.0.12",
+				"emotion-theming": "^10.0.10",
+				"linkifyjs": "^2.1.6",
+				"react-inspector": "^5.1.0"
+			},
+			"peerDependencies": {
+				"react": "^15.x || ^16.x || ^17.x"
+			}
+		},
+		"node_modules/console-feed/node_modules/linkifyjs": {
+			"version": "2.1.9",
+			"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.9.tgz",
+			"integrity": "sha512-74ivurkK6WHvHFozVaGtQWV38FzBwSTGNmJolEgFp7QgR2bl6ArUWlvT4GcHKbPe1z3nWYi+VUdDZk16zDOVug==",
+			"peerDependencies": {
+				"jquery": ">= 1.11.0",
+				"react": ">= 0.14.0",
+				"react-dom": ">= 0.14.0"
+			}
+		},
 		"node_modules/constants-browserify": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@@ -12742,6 +12769,20 @@
 				"node": ">= 4"
 			}
 		},
+		"node_modules/emotion-theming": {
+			"version": "10.3.0",
+			"resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.3.0.tgz",
+			"integrity": "sha512-mXiD2Oj7N9b6+h/dC6oLf9hwxbtKHQjoIqtodEyL8CpkN4F3V4IK/BT4D0C7zSs4BBFOu4UlPJbvvBLa88SGEA==",
+			"dependencies": {
+				"@babel/runtime": "^7.5.5",
+				"@emotion/weak-memoize": "0.2.5",
+				"hoist-non-react-statics": "^3.3.0"
+			},
+			"peerDependencies": {
+				"@emotion/core": "^10.0.27",
+				"react": ">=16.3.0"
+			}
+		},
 		"node_modules/encodeurl": {
 			"version": "1.0.2",
 			"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -19294,6 +19335,12 @@
 			"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz",
 			"integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q=="
 		},
+		"node_modules/jquery": {
+			"version": "3.6.0",
+			"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
+			"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
+			"peer": true
+		},
 		"node_modules/js-tokens": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -20005,6 +20052,14 @@
 				"url": "https://github.com/chalk/chalk?sponsor=1"
 			}
 		},
+		"node_modules/logdown": {
+			"version": "3.3.1",
+			"resolved": "https://registry.npmjs.org/logdown/-/logdown-3.3.1.tgz",
+			"integrity": "sha512-pjX0vlIJsYQlgVzFba2amXI1wZZnhrEorL68MdLI7B0/sN1TNUozBNFaHfcPHMM3A+INZ0OXFDxtnoaEgOmGjQ==",
+			"dependencies": {
+				"chalk": "^2.3.0"
+			}
+		},
 		"node_modules/loglevel": {
 			"version": "1.7.1",
 			"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
@@ -41428,6 +41483,26 @@
 			"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
 			"dev": true
 		},
+		"console-feed": {
+			"version": "3.3.0",
+			"resolved": "https://registry.npmjs.org/console-feed/-/console-feed-3.3.0.tgz",
+			"integrity": "sha512-GS0EtpiLyAZGEYBtTih+uI3s3NDmOsfkgpNGhr7UWeM5BzDT+dKgit2nEMFwDb2w7NaT95774/cwAztA1BxrHQ==",
+			"requires": {
+				"@emotion/core": "^10.0.10",
+				"@emotion/styled": "^10.0.12",
+				"emotion-theming": "^10.0.10",
+				"linkifyjs": "^2.1.6",
+				"react-inspector": "^5.1.0"
+			},
+			"dependencies": {
+				"linkifyjs": {
+					"version": "2.1.9",
+					"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.9.tgz",
+					"integrity": "sha512-74ivurkK6WHvHFozVaGtQWV38FzBwSTGNmJolEgFp7QgR2bl6ArUWlvT4GcHKbPe1z3nWYi+VUdDZk16zDOVug==",
+					"requires": {}
+				}
+			}
+		},
 		"constants-browserify": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@@ -42868,6 +42943,16 @@
 			"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
 			"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
 		},
+		"emotion-theming": {
+			"version": "10.3.0",
+			"resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.3.0.tgz",
+			"integrity": "sha512-mXiD2Oj7N9b6+h/dC6oLf9hwxbtKHQjoIqtodEyL8CpkN4F3V4IK/BT4D0C7zSs4BBFOu4UlPJbvvBLa88SGEA==",
+			"requires": {
+				"@babel/runtime": "^7.5.5",
+				"@emotion/weak-memoize": "0.2.5",
+				"hoist-non-react-statics": "^3.3.0"
+			}
+		},
 		"encodeurl": {
 			"version": "1.0.2",
 			"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -47801,6 +47886,12 @@
 			"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz",
 			"integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q=="
 		},
+		"jquery": {
+			"version": "3.6.0",
+			"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
+			"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
+			"peer": true
+		},
 		"js-tokens": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -48383,6 +48474,14 @@
 				}
 			}
 		},
+		"logdown": {
+			"version": "3.3.1",
+			"resolved": "https://registry.npmjs.org/logdown/-/logdown-3.3.1.tgz",
+			"integrity": "sha512-pjX0vlIJsYQlgVzFba2amXI1wZZnhrEorL68MdLI7B0/sN1TNUozBNFaHfcPHMM3A+INZ0OXFDxtnoaEgOmGjQ==",
+			"requires": {
+				"chalk": "^2.3.0"
+			}
+		},
 		"loglevel": {
 			"version": "1.7.1",
 			"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
diff --git a/packages/editor/package.json b/packages/editor/package.json
index c6c233d9a..560be7734 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -99,7 +99,8 @@
     "y-protocols": "^1.0.5",
     "yjs": "^13.5.16",
     "zxcvbn": "^4.4.2",
-    "react-router-dom": "^6.2.2"
+    "react-router-dom": "^6.2.2",
+    "console-feed": "^3.3.0"
   },
   "scripts": {
     "copytypes:self": "rimraf public/types && tsc --declaration --stripInternal --emitDeclarationOnly --noEmit false --declarationDir public/types/@typecell-org/editor",
diff --git a/packages/editor/src/runtime/executor/components/Console.tsx b/packages/editor/src/runtime/executor/components/Console.tsx
new file mode 100644
index 000000000..31690205d
--- /dev/null
+++ b/packages/editor/src/runtime/executor/components/Console.tsx
@@ -0,0 +1,60 @@
+import { ObservableMap } from "mobx";
+import { observer } from "mobx-react-lite";
+import React from "react";
+import { ConsoleOutput } from "./ConsoleOutput";
+import { Console as ConsoleComponent } from "console-feed";
+
+type Props = {
+  modelPath: string;
+  outputs: ObservableMap<string, ConsoleOutput>;
+};
+
+const Console: React.FC<Props> = observer((props) => {
+  const consoleOutput = props.outputs.get(props.modelPath);
+
+  let output = (consoleOutput?.events || []).map((event, i) => {
+    return {
+      id: event.id,
+      data: event.arguments,
+      method: event.method,
+    };
+  });
+
+  // Return blank in case there are no console events
+  if (!output.length) {
+    return <></>;
+  }
+
+  return (
+    <>
+      <div style={{ minHeight: "100px", width: "40%" }}></div>
+      <div style={consoleStyle}>
+        <ConsoleComponent
+          styles={{
+            LOG_AMOUNT_COLOR: "white",
+            // Somehow unable to change the amount background color. Line below doesn't work
+            LOG_INFO_AMOUNT_BACKROUND: "#0060ff",
+          }}
+          logs={output}
+          variant="light"
+        />
+      </div>
+    </>
+  );
+});
+
+const consoleStyle = {
+  borderLeft: "1px solid #eeeeee",
+  width: "40%",
+  maxHeight: "100%",
+  height: "100%",
+  overflow: "auto",
+  display: "flex",
+  "flex-direction": "column-reverse",
+  position: "absolute" as "absolute",
+  bottom: "-1px",
+  right: "0",
+  backgroundColor: "white",
+};
+
+export default Console;
diff --git a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts
new file mode 100644
index 000000000..67223d5b2
--- /dev/null
+++ b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts
@@ -0,0 +1,50 @@
+import { makeObservable, observable, runInAction } from "mobx";
+import { lifecycle } from "vscode-lib";
+import { ConsolePayload } from "../../../../../engine/types/Engine";
+
+interface ConsoleEvent extends ConsolePayload {
+  id: string;
+}
+
+/**
+ * Keeps track of console output for a cell. Appends new events to the events array.
+ */
+export class ConsoleOutput extends lifecycle.Disposable {
+  private autorunDisposer: (() => void) | undefined;
+  // Keep track of id's so every new event always has a unique id.
+  private idIncrement = 1;
+  public events: ConsoleEvent[] = [];
+
+  constructor() {
+    super();
+    makeObservable(this, {
+      events: observable.shallow,
+    });
+  }
+
+  public async appendEvent(consolePayload: ConsolePayload) {
+    runInAction(() => {
+      if (consolePayload.method === "clear") {
+        this.events = [];
+      } else {
+        if (this.events.length >= 999) {
+          // Remove the first event when this arbitrary limit is reached to prevent memory issues.
+          this.events.shift();
+        }
+
+        this.idIncrement++;
+        this.events.push({
+          id: this.idIncrement.toString(),
+          ...consolePayload,
+        });
+      }
+    });
+  }
+
+  public dispose() {
+    if (this.autorunDisposer) {
+      this.autorunDisposer();
+    }
+    super.dispose();
+  }
+}
diff --git a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx
index e525f6f08..5b18370f7 100644
--- a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx
+++ b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef } from "react";
 import Output from "../../../components/Output";
 import { FrameConnection } from "./FrameConnection";
 import "./Frame.css";
+import Console from "../../../components/Console";
 
 // The sandbox frame where end-user code gets evaluated.
 // It is loaded from index.iframe.ts
@@ -106,8 +107,9 @@ export const Frame = observer((props: {}) => {
               style={getOutputOuterStyle(positions.x, positions.y)}
               onMouseMove={onMouseMoveOutput}>
               <div style={outputInnerStyle}>
-                <Output modelPath={id} outputs={connection.outputs} />
+                <Output modelPath={id} outputs={connection.modelOutputs} />
               </div>
+              <Console modelPath={id} outputs={connection.consoleOutputs} />
             </div>
           );
         })}
@@ -122,11 +124,12 @@ const getOutputOuterStyle = (x: number, y: number) => ({
   position: "absolute" as "absolute",
   padding: "10px",
   width: "100%",
+  display: "flex",
 });
 
 const outputInnerStyle = {
-  maxWidth: "100%",
-  width: "100%",
+  overflow: "auto",
+  flex: "1",
 };
 
 const containerStyle = { position: "relative" as "relative" };
diff --git a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts
index 8f65c5814..cc2dfd13f 100644
--- a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts
+++ b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts
@@ -5,11 +5,11 @@ import { lifecycle } from "vscode-lib";
 import { CompiledCodeModel } from "../../../../../models/CompiledCodeModel";
 import { getTypeCellResolver } from "../../../resolver/resolver";
 import { ModelOutput } from "../../../components/ModelOutput";
-
 import { ModelReceiver } from "./ModelReceiver";
 import type { VisualizersByPath } from "../../../../extensions/visualizer/VisualizerExtension";
 import { IframeBridgeMethods } from "./IframeBridgeMethods";
 import { HostBridgeMethods } from "../HostBridgeMethods";
+import { ConsoleOutput } from "../../../components/ConsoleOutput";
 
 let ENGINE_ID = 0;
 
@@ -21,11 +21,24 @@ export class FrameConnection extends lifecycle.Disposable {
   public readonly id = ENGINE_ID++;
 
   /**
-   * Map of <cellPath, ModelOutput> that keeps track of the variables exported by every cell
+   * Map of <cellPath, ModelOutput> that keeps track of the generated output for every cell
    */
-  public readonly outputs = observable.map<string, ModelOutput>(undefined, {
-    deep: false,
-  });
+  public readonly modelOutputs = observable.map<string, ModelOutput>(
+    undefined,
+    {
+      deep: false,
+    }
+  );
+
+  /**
+   * Map of <cellPath, ConsoleOutput> that keeps track of console output for every cell
+   */
+  public readonly consoleOutputs = observable.map<string, ConsoleOutput>(
+    undefined,
+    {
+      deep: false,
+    }
+  );
 
   /**
    * Map of <cellPath, { x, y }> that keeps track of the positions of every cell.
@@ -80,15 +93,26 @@ export class FrameConnection extends lifecycle.Disposable {
     // pass the code to the engine by acting as a ModelProvider
     this.engine.registerModelProvider(mainModelReceiver);
 
+    this._register(
+      this.engine.onConsole(({ model, payload }) => {
+        let consoleOutput = this.consoleOutputs.get(model.path);
+        if (!consoleOutput) {
+          consoleOutput = this._register(new ConsoleOutput());
+          this.consoleOutputs.set(model.path, consoleOutput);
+        }
+        consoleOutput.appendEvent(payload);
+      })
+    );
+
     // Listen to outputs of evaluated cells
     this._register(
       this.engine.onOutput(({ model, output }) => {
-        let modelOutput = this.outputs.get(model.path);
+        let modelOutput = this.modelOutputs.get(model.path);
         if (!modelOutput) {
           modelOutput = this._register(
             new ModelOutput(this.engine.observableContext.context)
           );
-          this.outputs.set(model.path, modelOutput);
+          this.modelOutputs.set(model.path, modelOutput);
         }
         modelOutput.updateValue(output);
       })
@@ -231,7 +255,7 @@ export class FrameConnection extends lifecycle.Disposable {
     // For type visualizers (experimental)
     updateVisualizers: async (e: VisualizersByPath) => {
       for (let [path, visualizers] of Object.entries(e)) {
-        this.outputs.get(path)!.updateVisualizers(visualizers);
+        this.modelOutputs.get(path)!.updateVisualizers(visualizers);
       }
     },
   };
diff --git a/packages/engine/package.json b/packages/engine/package.json
index 36920228c..93cc6d5df 100644
--- a/packages/engine/package.json
+++ b/packages/engine/package.json
@@ -5,6 +5,7 @@
   "dependencies": {
     "es-module-shims": "1.4.3",
     "lodash": "^4.17.21",
+    "logdown": "^3.3.1",
     "mobx": "^6.2.0",
     "react": "^17.0.2",
     "vscode-lib": "^0.1.2"
diff --git a/packages/engine/src/CellEvaluator.ts b/packages/engine/src/CellEvaluator.ts
index fb9cb9af4..2b522c1b6 100644
--- a/packages/engine/src/CellEvaluator.ts
+++ b/packages/engine/src/CellEvaluator.ts
@@ -1,4 +1,5 @@
 import { TypeCellContext } from "./context";
+import { ConsolePayload } from "./Engine";
 import { ModuleExecution, runModule } from "./executor";
 import { HookExecution } from "./HookExecution";
 import { createExecutionScope, getModulesFromTypeCellCode } from "./modules";
@@ -10,7 +11,8 @@ export function createCellEvaluator(
   typecellContext: TypeCellContext<any>,
   resolveImport: (module: string) => Promise<any>,
   setAndWatchOutput = true,
-  onOutputChanged: (output: any) => void,
+  onOutputEvent: (output: any) => void,
+  onConsoleEvent: (console: ConsolePayload) => void,
   beforeExecuting: () => void
 ) {
   function onExecuted(exports: any) {
@@ -42,15 +44,15 @@ export function createCellEvaluator(
         });
       }
     }
-    onOutputChanged(newExports);
+    onOutputEvent(newExports);
   }
 
   function onError(error: any) {
     // log.warn("cellEvaluator onError", cell.path, error);
-    onOutputChanged(error);
+    onOutputEvent(error);
   }
 
-  const hookExecution = new HookExecution();
+  const hookExecution = new HookExecution(onConsoleEvent);
   const executionScope = createExecutionScope(
     typecellContext,
     hookExecution.scopeHooks
@@ -84,7 +86,7 @@ export function createCellEvaluator(
     } catch (e) {
       console.error(e);
       // log.warn("cellEvaluator error evaluating", cell.path, e);
-      onOutputChanged(e);
+      onOutputEvent(e);
     }
   }
 
diff --git a/packages/engine/src/Engine.test.ts b/packages/engine/src/Engine.test.ts
index f4e5164a4..442e7f23e 100644
--- a/packages/engine/src/Engine.test.ts
+++ b/packages/engine/src/Engine.test.ts
@@ -7,92 +7,187 @@ import {
   toAMDFormat,
   waitTillEvent,
 } from "./tests/util/helpers";
-
-const getModel1 = () =>
-  buildMockedModel(
-    "model1",
-    `let x = 4; 
+import { CodeModelMock } from "./tests/util/CodeModelMock";
+
+describe("engine class execution", function () {
+  describe("basic model execution", () => {
+    const getModel1 = () =>
+      buildMockedModel(
+        "model1",
+        `let x = 4; 
 let y = 6; 
 let sum = x + y;
 exports.sum = sum;
 exports.default = sum;`
-  );
+      );
 
-const getModel2 = () =>
-  buildMockedModel("model2", `exports.default = $.sum - 5;`);
+    const getModel2 = () =>
+      buildMockedModel("model2", `exports.default = $.sum - 5;`);
 
-describe("engine class", () => {
-  it("should execute a single model", async () => {
-    const engine = new Engine<CodeModel>(importResolver);
-    engine.registerModel(getModel1());
+    it("should execute a single model", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      engine.registerModel(getModel1());
 
-    const { model, output } = await event.Event.toPromise(engine.onOutput);
+      const { model, output } = await event.Event.toPromise(engine.onOutput);
 
-    expect(model.path).toBe("model1");
-    expect(output.sum).toBe(10);
-    expect(output.default).toBe(10);
-  });
+      expect(model.path).toBe("model1");
+      expect(output.sum).toBe(10);
+      expect(output.default).toBe(10);
+    });
 
-  it("should read exported variables from other models", async () => {
-    const engine = new Engine<CodeModel>(importResolver);
-    engine.registerModel(getModel1());
-    await event.Event.toPromise(engine.onOutput);
+    it("should read exported variables from other models", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      engine.registerModel(getModel1());
+      await event.Event.toPromise(engine.onOutput);
 
-    engine.registerModel(getModel2());
-    const { model, output } = await event.Event.toPromise(engine.onOutput);
+      engine.registerModel(getModel2());
+      const { model, output } = await event.Event.toPromise(engine.onOutput);
 
-    expect(model.path).toBe("model2");
-    expect(output.default).toBe(5);
-  });
+      expect(model.path).toBe("model2");
+      expect(output.default).toBe(5);
+    });
 
-  it("should re-evaluate code after change", async () => {
-    const engine = new Engine<CodeModel>(importResolver);
-    const model1 = getModel1();
+    it("should re-evaluate code after change", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      const model1 = getModel1();
 
-    engine.registerModel(model1);
-    await event.Event.toPromise(engine.onOutput);
+      engine.registerModel(model1);
+      await event.Event.toPromise(engine.onOutput);
 
-    model1.updateCode(
-      toAMDFormat(`let x = 0;
+      model1.updateCode(
+        toAMDFormat(`let x = 0;
     let y = 6;
     let sum = x + y;
     exports.sum = sum;
     exports.default = sum;`)
-    );
+      );
 
-    const { output } = await event.Event.toPromise(engine.onOutput);
+      const { output } = await event.Event.toPromise(engine.onOutput);
 
-    expect(output.sum).toBe(6);
-    expect(output.default).toBe(6);
-  });
+      expect(output.sum).toBe(6);
+      expect(output.default).toBe(6);
+    });
 
-  it("should re-evaluate other models when global variable changes", async () => {
-    const engine = new Engine<CodeModel>(importResolver);
-    // TODO: Expected 4 events. Figure out why model 2 re-evaluates.
-    const eventsPromise = waitTillEvent(engine.onOutput, 5);
-    const model1 = getModel1();
-    const model2 = getModel2();
+    it("should re-evaluate other models when global variable changes", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      // TODO: Expected 4 events. Figure out why model 2 re-evaluates.
+      const eventsPromise = waitTillEvent(engine.onOutput, 5);
+      const model1 = getModel1();
+      const model2 = getModel2();
 
-    engine.registerModel(model1);
-    engine.registerModel(model2);
+      engine.registerModel(model1);
+      engine.registerModel(model2);
 
-    model1.updateCode(
-      toAMDFormat(`let x = 0;
+      model1.updateCode(
+        toAMDFormat(`let x = 0;
     let y = 6;
     let sum = x + y;
     exports.sum = sum;
     exports.default = sum;`)
-    );
-
-    const events = await eventsPromise;
-    const eventsSnapshot = events.map((event) => ({
-      path: event.model.path,
-      output: event.output,
-    }));
-    const finalEvent = eventsSnapshot[eventsSnapshot.length - 1];
-
-    expect(finalEvent.path).toBe("model2");
-    expect(finalEvent.output.default).toBe(1);
-    expect(eventsSnapshot).toMatchSnapshot();
+      );
+
+      const events = await eventsPromise;
+      const eventsSnapshot = events.map((event) => ({
+        path: event.model.path,
+        output: event.output,
+      }));
+      const finalEvent = eventsSnapshot[eventsSnapshot.length - 1];
+
+      expect(finalEvent.path).toBe("model2");
+      expect(finalEvent.output.default).toBe(1);
+      expect(eventsSnapshot).toMatchSnapshot();
+    });
+  });
+
+  describe("console messages", () => {
+    const getModel1 = () => buildMockedModel("model1", `console.log('hi!');`);
+    const getModel2 = () =>
+      buildMockedModel(
+        "model2",
+        `console.info('info'); console.warn('warn'); console.error('error');`
+      );
+    const getModel3 = () =>
+      buildMockedModel(
+        "model3",
+        `console.log('before');
+        await new Promise((resolve)=> {
+          setTimeout(()=> {
+            resolve();
+          }, 1)
+        });
+        console.log('after');`
+      );
+    const getModel4 = () =>
+      new CodeModelMock(
+        "javascript",
+        "model4",
+        `define(["require", "exports", "logdown"], function(require, exports, logdown) {
+          "use strict";
+          Object.defineProperty(exports, "__esModule", { value: true });         
+        
+          let logger = logdown("logger 1");
+          logger.state.isEnabled = true;
+          logger.log("message 1");
+
+          setTimeout(() => {
+            logger.state.isEnabled = true;
+            logger.log("message 2");
+          }, 1);
+        });`
+      );
+
+    it("should capture console.log message", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      const eventsPromise = waitTillEvent(engine.onConsole, 1);
+      const model1 = getModel1();
+
+      engine.registerModel(model1);
+
+      const consoleEvents = await eventsPromise;
+
+      expect(consoleEvents[0].payload.method).toBe("log");
+      expect(consoleEvents[0].payload.arguments[0]).toBe("hi!");
+    });
+
+    it("should capture console.warn/info/error messages", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      const eventsPromise = waitTillEvent(engine.onConsole, 3);
+      const model2 = getModel2();
+
+      engine.registerModel(model2);
+
+      const events = await eventsPromise;
+      const eventsSnapshot = events.map((event) => {
+        return {
+          path: event.model.path,
+          console: event.payload,
+        };
+      });
+      expect(eventsSnapshot).toMatchSnapshot();
+    });
+
+    it("should capture console.log messages after async", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      const eventsPromise = waitTillEvent(engine.onConsole, 2);
+      const model3 = getModel3();
+
+      engine.registerModel(model3);
+
+      const events = await eventsPromise;
+      expect(events[0].payload.arguments[0]).toBe("before");
+      expect(events[1].payload.arguments[0]).toBe("after");
+    });
+
+    it("should capture console.log messages from library (sync only)", async () => {
+      const engine = new Engine<CodeModel>(importResolver);
+      const eventsPromise = waitTillEvent(engine.onConsole, 2);
+      const model4 = getModel4();
+
+      engine.registerModel(model4);
+
+      const events = await eventsPromise;
+      expect(events[0].payload.arguments[1]).toBe("message 1");
+      expect(events[1].payload.arguments[1]).toBe("message 2");
+    });
   });
 });
diff --git a/packages/engine/src/Engine.ts b/packages/engine/src/Engine.ts
index 1b92df8bf..742870c82 100644
--- a/packages/engine/src/Engine.ts
+++ b/packages/engine/src/Engine.ts
@@ -8,6 +8,32 @@ export type ResolvedImport = {
   module: any;
 } & lifecycle.IDisposable;
 
+export type OutputEvent<T> = {
+  model: T;
+  output: any;
+};
+
+export type ConsolePayload = {
+  method:
+    | "log"
+    | "debug"
+    | "info"
+    | "warn"
+    | "error"
+    | "table"
+    | "clear"
+    | "time"
+    | "timeEnd"
+    | "count"
+    | "assert";
+  arguments: any[];
+};
+
+export type ConsoleEvent<T> = {
+  model: T;
+  payload: ConsolePayload;
+};
+
 /**
  * The engine automatically runs models registered to it.
  * The code of the models is passed a context ($) provided by the engine.
@@ -26,17 +52,30 @@ export class Engine<T extends CodeModel> extends lifecycle.Disposable {
     ReturnType<typeof createCellEvaluator>
   >();
 
-  private readonly _onOutput: event.Emitter<{ model: T; output: any }> =
-    this._register(new event.Emitter<{ model: T; output: any }>());
+  private readonly _onOutput: event.Emitter<OutputEvent<T>> = this._register(
+    new event.Emitter<OutputEvent<T>>()
+  );
+
+  private readonly _onConsole: event.Emitter<ConsoleEvent<T>> = this._register(
+    new event.Emitter<ConsoleEvent<T>>()
+  );
 
   /**
    * Raised whenever a model is (re)evaluated, with the exports by that model
    *
-   * @type {event.Event<{ model: T; output: any }>}
+   * @type {event.Event<OutputEvent<T>>}
+   * @memberof Engine
+   */
+  public readonly onOutput: event.Event<OutputEvent<T>> = this._onOutput.event;
+
+  /**
+   * Raised whenever a model calls console.* functions
+   *
+   * @type {event.Event<ConsoleEvent<T>>}
    * @memberof Engine
    */
-  public readonly onOutput: event.Event<{ model: T; output: any }> =
-    this._onOutput.event;
+  public readonly onConsole: event.Event<ConsoleEvent<T>> =
+    this._onConsole.event;
 
   private readonly _onBeforeExecution: event.Emitter<{ model: T }> =
     this._register(new event.Emitter<{ model: T }>());
@@ -103,15 +142,24 @@ export class Engine<T extends CodeModel> extends lifecycle.Disposable {
           }
           return ret.module;
         },
-        (model, output) => this._onOutput.fire({ model, output })
+        (event) => this._onOutput.fire(event),
+        (event) => this._onConsole.fire(event)
       ); // catch errors?
     };
     let prevValue: string | undefined = model.getValue();
 
     // TODO: maybe only debounce (or increase debounce timeout) if an execution is still pending?
     const reEvaluate = _.debounce(() => {
+      // make sure there were actual changes from the previous value
       if (model.getValue() !== prevValue) {
-        // make sure there were actual changes from the previous value
+        // Clear the console upon re-evaluation
+        this._onConsole.fire({
+          model,
+          payload: {
+            method: "clear",
+            arguments: [],
+          },
+        });
 
         prevValue = model.getValue();
         evaluate();
@@ -152,7 +200,8 @@ export class Engine<T extends CodeModel> extends lifecycle.Disposable {
     model: T,
     typecellContext: TypeCellContext<any>,
     resolveImport: (module: string) => Promise<any>,
-    onOutput: (model: T, output: any) => void
+    onOutput: (event: OutputEvent<T>) => void,
+    onConsole: (event: ConsoleEvent<T>) => void
   ) {
     if (!this.evaluatorCache.has(model)) {
       this.evaluatorCache.set(
@@ -161,7 +210,8 @@ export class Engine<T extends CodeModel> extends lifecycle.Disposable {
           typecellContext,
           resolveImport,
           true,
-          (output) => onOutput(model, output),
+          (output) => onOutput({ model, output }),
+          (console) => onConsole({ model, payload: console }),
           () => this._onBeforeExecution.fire({ model })
         )
       );
diff --git a/packages/engine/src/HookExecution.ts b/packages/engine/src/HookExecution.ts
index a182d85a9..340c512b8 100644
--- a/packages/engine/src/HookExecution.ts
+++ b/packages/engine/src/HookExecution.ts
@@ -1,3 +1,5 @@
+import { ConsolePayload } from "./Engine";
+
 const glob = typeof window === "undefined" ? global : window;
 
 // These functions will be added to the scope of the cell
@@ -21,6 +23,10 @@ export type ScopeHooks = { [K in typeof onScopeFunctions[number]]: any };
 
 export type WindowHooks = { [K in typeof onWindowFunctions[number]]: any };
 
+/**
+ * Sets object property based on a given path and value.
+ * E.g. path could be level1.level2.prop
+ */
 function setProperty(base: Object, path: string, value: any) {
   const layers = path.split(".");
   if (layers.length > 1) {
@@ -44,10 +50,61 @@ export class HookExecution {
     }),
     console: {
       ...originalReferences.console,
-      log: (...args: any) => {
-        // TODO: broadcast output to console view
-        originalReferences.console.log(...args);
-      },
+      log: (...args: any) =>
+        this.onConsoleEvent({
+          method: "log",
+          arguments: args,
+        }),
+      debug: (...args: any) =>
+        this.onConsoleEvent({
+          method: "debug",
+          arguments: args,
+        }),
+      info: (...args: any) =>
+        this.onConsoleEvent({
+          method: "info",
+          arguments: args,
+        }),
+      warn: (...args: any) =>
+        this.onConsoleEvent({
+          method: "warn",
+          arguments: args,
+        }),
+      error: (...args: any) =>
+        this.onConsoleEvent({
+          method: "error",
+          arguments: args,
+        }),
+      table: (...args: any) =>
+        this.onConsoleEvent({
+          method: "table",
+          arguments: args,
+        }),
+      clear: (...args: any) =>
+        this.onConsoleEvent({
+          method: "clear",
+          arguments: args,
+        }),
+      time: (...args: any) =>
+        this.onConsoleEvent({
+          method: "time",
+          arguments: args,
+        }),
+      timeEnd: (...args: any) =>
+        this.onConsoleEvent({
+          method: "timeEnd",
+          arguments: args,
+        }),
+      count: (...args: any) =>
+        this.onConsoleEvent({
+          method: "count",
+          arguments: args,
+        }),
+      assert: (...args: any) =>
+        this.onConsoleEvent({
+          method: "assert",
+          arguments: args,
+        }),
     },
   };
 
@@ -56,7 +113,7 @@ export class HookExecution {
     ["EventTarget.prototype.addEventListener"]: undefined,
   };
 
-  constructor() {
+  constructor(private onConsoleEvent: (console: ConsolePayload) => void) {
     if (typeof EventTarget !== "undefined") {
       this.windowHooks["EventTarget.prototype.addEventListener"] =
         this.createHookedFunction(
@@ -85,7 +142,7 @@ export class HookExecution {
   }
 
   private createHookedFunction<T, Y>(
-    original: (...args: T[]) => Y,
+    original: (...args: any[]) => Y,
     disposer: (ret: Y, args: T[]) => void
   ) {
     const self = this;
diff --git a/packages/engine/src/__snapshots__/Engine.test.ts.snap b/packages/engine/src/__snapshots__/Engine.test.ts.snap
index 7deb1487a..fba8fdf73 100644
--- a/packages/engine/src/__snapshots__/Engine.test.ts.snap
+++ b/packages/engine/src/__snapshots__/Engine.test.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`engine class should re-evaluate other models when global variable changes 1`] = `
+exports[`engine class execution basic model execution should re-evaluate other models when global variable changes 1`] = `
 Array [
   Object {
     "output": Object {
@@ -34,3 +34,35 @@ Array [
   },
 ]
 `;
+
+exports[`engine class execution console messages should capture console.warn/info/error messages 1`] = `
+Array [
+  Object {
+    "console": Object {
+      "arguments": Array [
+        "info",
+      ],
+      "method": "info",
+    },
+    "path": "model2",
+  },
+  Object {
+    "console": Object {
+      "arguments": Array [
+        "warn",
+      ],
+      "method": "warn",
+    },
+    "path": "model2",
+  },
+  Object {
+    "console": Object {
+      "arguments": Array [
+        "error",
+      ],
+      "method": "error",
+    },
+    "path": "model2",
+  },
+]
+`;
diff --git a/packages/engine/src/tests/util/helpers.ts b/packages/engine/src/tests/util/helpers.ts
index 79a7bf409..e51c5086e 100644
--- a/packages/engine/src/tests/util/helpers.ts
+++ b/packages/engine/src/tests/util/helpers.ts
@@ -1,6 +1,7 @@
 import { CodeModel } from "../../CodeModel";
 import { ResolvedImport } from "../../Engine";
 import { CodeModelMock } from "./CodeModelMock";
+import * as logdown from "logdown";
 
 export function waitTillEvent<T>(
   e: (listener: (arg0: T) => void) => void,
@@ -20,9 +21,18 @@ export function waitTillEvent<T>(
 }
 
 export async function importResolver(
-  _module: string,
+  module: string,
   _forModel: CodeModel
 ): Promise<ResolvedImport> {
+  if (module === "logdown") {
+    return (async () => {
+      return {
+        module: logdown.default,
+        dispose: () => {},
+      };
+    })();
+  }
+
   const res = async () => {
     return {
       module: {