diff --git a/engines/execution-controller.ts b/engines/execution-controller.ts
index 8f89fc0..e00de83 100644
--- a/engines/execution-controller.ts
+++ b/engines/execution-controller.ts
@@ -134,6 +134,16 @@ class ExecutionController<RS> {
     return this._result;
   }
 
+  /**
+   * Run a single step of execution
+   * @returns Result of execution
+   */
+  executeStep(): StepExecutionResult<RS> {
+    this._result = this._engine.executeStep();
+    this._result.signal = "paused";
+    return this._result;
+  }
+
   /** Asynchronously sleep for a period of time */
   private async sleep(millis: number) {
     return new Promise<void>((resolve) => setTimeout(resolve, millis));
diff --git a/engines/worker-constants.ts b/engines/worker-constants.ts
index cec0a74..0091899 100644
--- a/engines/worker-constants.ts
+++ b/engines/worker-constants.ts
@@ -22,6 +22,10 @@ export type WorkerRequestData =
       type: "Execute";
       params: { interval?: number };
     }
+  | {
+      type: "ExecuteStep";
+      params?: null;
+    }
   | {
       type: "Pause";
       params?: null;
diff --git a/engines/worker.ts b/engines/worker.ts
index 94826f1..3840fb4 100644
--- a/engines/worker.ts
+++ b/engines/worker.ts
@@ -64,7 +64,6 @@ const updateBreakpoints = (points: number[]) => {
  * and return result of execution.
  */
 const execute = (interval?: number) => {
-  console.info(`Executing at interval ${interval}`);
   _controller!.executeAll({
     interval,
     onResult: (res) => postMessage(resultMessage(res)),
@@ -79,12 +78,21 @@ const pauseExecution = async () => {
   postMessage(ackMessage("pause"));
 };
 
+/**
+ * Run a single execution step
+ */
+const executeStep = () => {
+  const result = _controller!.executeStep();
+  postMessage(resultMessage(result));
+};
+
 addEventListener("message", async (ev: MessageEvent<WorkerRequestData>) => {
   if (ev.data.type === "Init") return initController();
   if (ev.data.type === "Reset") return resetController();
   if (ev.data.type === "Prepare") return prepare(ev.data.params);
   if (ev.data.type === "Execute") return execute(ev.data.params.interval);
   if (ev.data.type === "Pause") return await pauseExecution();
+  if (ev.data.type === "ExecuteStep") return executeStep();
   if (ev.data.type === "UpdateBreakpoints")
     return updateBreakpoints(ev.data.params.points);
   throw new Error("Invalid worker message type");
diff --git a/ui/Mainframe.tsx b/ui/Mainframe.tsx
index c586a45..dc7c1b6 100644
--- a/ui/Mainframe.tsx
+++ b/ui/Mainframe.tsx
@@ -57,6 +57,21 @@ export const Mainframe = () => {
     await execController.pauseExecution();
   };
 
+  /** Run a single step of execution */
+  const executeStep = async () => {
+    // Check if controller is paused
+    if (execController.state !== "paused") {
+      console.error("Controller not paused");
+      return;
+    }
+
+    // Run and update execution states
+    const result = await execController.executeStep();
+    setRendererState(result.rendererState);
+    setCodeHighlights(result.nextStepLocation || undefined);
+    setOutput((o) => (o || "") + (result.output || ""));
+  };
+
   /** Resume the currently paused execution */
   const resumeExecution = async () => {
     // Check if controller is indeed paused
@@ -125,6 +140,7 @@ export const Mainframe = () => {
           onRun={runProgram}
           onPause={pauseExecution}
           onResume={resumeExecution}
+          onStep={executeStep}
           onStop={stopExecution}
         />
       )}
diff --git a/ui/execution-controls.tsx b/ui/execution-controls.tsx
index 3942bc4..983054d 100644
--- a/ui/execution-controls.tsx
+++ b/ui/execution-controls.tsx
@@ -24,6 +24,7 @@ const DebugControls = (props: {
   paused: boolean;
   onPause: () => void;
   onResume: () => void;
+  onStep: () => void;
   onStop: () => void;
 }) => {
   return (
@@ -37,6 +38,7 @@ const DebugControls = (props: {
       <Button
         small
         title="Step"
+        onClick={props.onStep}
         disabled={!props.paused}
         icon={<Icon icon="step-forward" intent="warning" />}
       />
@@ -55,6 +57,7 @@ type Props = {
   onRun: () => void;
   onPause: () => void;
   onResume: () => void;
+  onStep: () => void;
   onStop: () => void;
 };
 
@@ -68,6 +71,7 @@ export const ExecutionControls = (props: Props) => {
           paused={props.state === "paused"}
           onPause={props.onPause}
           onResume={props.onResume}
+          onStep={props.onStep}
           onStop={props.onStop}
         />
       )}
diff --git a/ui/use-exec-controller.ts b/ui/use-exec-controller.ts
index d4f6adc..cb39d8f 100644
--- a/ui/use-exec-controller.ts
+++ b/ui/use-exec-controller.ts
@@ -140,6 +140,20 @@ export const useExecController = <RS>() => {
     });
   }, []);
 
+  /**
+   * Run a single step of execution
+   * @return Execution result
+   */
+  const executeStep = React.useCallback(async () => {
+    const res = await requestWorker(
+      { type: "ExecuteStep" },
+      (res) => res.type !== "result"
+    );
+    if (res.type !== "result") throw new Error("Something unexpected happened");
+    if (!res.data.nextStepLocation) setWorkerState("done");
+    return res.data;
+  }, []);
+
   /**
    * Execute the code loaded into the engine
    * @param onResult Callback used when an execution result is received
@@ -172,6 +186,7 @@ export const useExecController = <RS>() => {
     prepare,
     pauseExecution,
     execute,
+    executeStep,
     updateBreakpoints,
   };
 };