diff --git a/pyinfra/api/operation.py b/pyinfra/api/operation.py
index cc0252292..91a124f9d 100644
--- a/pyinfra/api/operation.py
+++ b/pyinfra/api/operation.py
@@ -14,7 +14,7 @@
 from pyinfra.context import ctx_host, ctx_state
 
 from .arguments import get_execution_kwarg_keys, pop_global_arguments
-from .command import EvalOperationAtExecution, FunctionCommand, StringCommand
+from .command import StringCommand
 from .exceptions import OperationValueError, PyinfraError
 from .host import Host
 from .operations import run_host_op
@@ -36,24 +36,16 @@
 class OperationMeta:
     combined_output_lines = None
 
-    def __init__(self, hash=None, commands=None):
-        # Wrap all the attributes
-        commands = commands or []
-        self.commands = commands
+    def __init__(self, hash=None, is_change=False):
         self.hash = hash
-
-        # Changed flag = did we do anything?
-        self.changed = len(self.commands) > 0
+        self.changed = is_change
 
     def __repr__(self):
         """
         Return Operation object as a string.
         """
 
-        return (
-            f"OperationMeta(commands={len(self.commands)}, "
-            f"changed={self.changed}, hash={self.hash})"
-        )
+        return f"OperationMeta(changed={self.changed}, hash={self.hash})"
 
     def set_combined_output_lines(self, combined_output_lines):
         self.combined_output_lines = combined_output_lines
@@ -200,30 +192,42 @@ def decorated_func(*args, **kwargs):
             if has_run:
                 return OperationMeta(op_hash)
 
-        # "Run" operation
-        #
+        # "Run" operation - here we make a generator that will yield out actual commands to execute
+        # and, if we're diff-ing, we then iterate the generator now to determine if any changes
+        # *would* be made based on the *current* remote state.
 
-        # Otherwise, flag as in-op and run it to get the commands
-        host.in_op = True
-        host.current_op_hash = op_hash
-        host.current_op_global_kwargs = global_kwargs
+        def command_generator():
+            host.in_op = True
+            host.current_op_hash = op_hash
+            host.current_op_global_kwargs = global_kwargs
 
-        # Convert to list as the result may be a generator
-        commands = [  # convert any strings -> StringCommand's
-            StringCommand(command.strip()) if isinstance(command, str) else command
-            for command in func(*args, **kwargs)
-        ]
+            for command in func(*args, **kwargs):
+                command = StringCommand(command.strip()) if isinstance(command, str) else command
+                yield command
 
-        host.in_op = False
-        host.current_op_hash = None
-        host.current_op_global_kwargs = None
+            host.in_op = False
+            host.current_op_hash = None
+            host.current_op_global_kwargs = None
 
-        if EvalOperationAtExecution in commands:
-            logger.warning("Defering operation evaluation until execution: %s", op_meta["names"])
-            commands = [FunctionCommand(operation(func), args, kwargs)]
+        op_is_change = False
+        if state.should_diff():
+            for command in command_generator():
+                op_is_change = True
+                break
 
         # Add host-specific operation data to state, this mutates state
-        operation_meta = _update_state_meta(state, host, commands, op_hash, op_meta, global_kwargs)
+        operation_meta = _add_host_op_to_state(
+            state,
+            host,
+            op_hash,
+            op_is_change,
+            command_generator,
+            global_kwargs,
+        )
+
+        # If we're already in the execution phase, execute this operation immediately
+        if state.is_executing:
+            _execute_immediately(state, host, op_hash)
 
         # Return result meta for use in deploy scripts
         return operation_meta
@@ -338,12 +342,14 @@ def _ensure_shared_op_meta(state, op_hash, op_order, global_kwargs, names):
     return op_meta
 
 
-def _execute_immediately(state, host, op_data, op_meta, op_hash):
+def _execute_immediately(state, host, op_hash):
     logger.warning(
         f"Note: nested operations are currently in beta ({get_call_location()})\n"
         "    More information: "
         "https://docs.pyinfra.com/en/2.x/using-operations.html#nested-operations",
     )
+    op_meta = state.get_op_meta(op_hash)
+    op_data = state.get_op_data_for_host(host, op_hash)
     op_data["parent_op_hash"] = host.executing_op_hash
     log_operation_start(op_meta, op_types=["nested"], prefix="")
     run_host_op(state, host, op_hash)
@@ -370,28 +376,24 @@ def _attach_args(op_meta, args, kwargs):
 
 
 # NOTE: this function mutates state.meta for this host
-def _update_state_meta(state, host, commands, op_hash, op_meta, global_kwargs):
-    # We're doing some commands, meta/ops++
-    state.meta[host]["ops"] += 1
-    state.meta[host]["commands"] += len(commands)
+def _add_host_op_to_state(state, host, op_hash, is_change, command_generator, global_kwargs):
+    host_meta = state.get_meta_for_host(host)
+
+    host_meta["ops"] += 1
 
-    if commands:
-        state.meta[host]["ops_change"] += 1
+    if is_change:
+        host_meta["ops_change"] += 1
     else:
-        state.meta[host]["ops_no_change"] += 1
+        host_meta["ops_no_change"] += 1
 
-    operation_meta = OperationMeta(op_hash, commands)
+    operation_meta = OperationMeta(op_hash, is_change)
 
     # Add the server-relevant commands
     op_data = {
-        "commands": commands,
+        "command_generator": command_generator,
         "global_kwargs": global_kwargs,
         "operation_meta": operation_meta,
     }
-    state.set_op_data(host, op_hash, op_data)
-
-    # If we're already in the execution phase, execute this operation immediately
-    if state.is_executing:
-        _execute_immediately(state, host, op_data, op_meta, op_hash)
+    state.set_op_data_for_host(host, op_hash, op_data)
 
     return operation_meta
diff --git a/pyinfra/api/operations.py b/pyinfra/api/operations.py
index 749929c62..41c37f1ca 100644
--- a/pyinfra/api/operations.py
+++ b/pyinfra/api/operations.py
@@ -41,7 +41,7 @@ def run_host_op(state: "State", host: "Host", op_hash):
         logger.info("{0}{1}".format(host.print_prefix, click.style("Skipped", "blue")))
         return True
 
-    op_data = state.get_op_data(host, op_hash)
+    op_data = state.get_op_data_for_host(host, op_hash)
     global_kwargs = op_data["global_kwargs"]
 
     op_meta = state.get_op_meta(op_hash)
@@ -114,7 +114,7 @@ def run_condition(condition_name: str) -> bool:
     executed_commands = 0
     all_combined_output_lines = []
 
-    for i, command in enumerate(op_data["commands"]):
+    for command in op_data["command_generator"]():
         status = False
 
         executor_kwargs = base_executor_kwargs.copy()
@@ -170,7 +170,7 @@ def run_condition(condition_name: str) -> bool:
         state.results[host]["ops"] += 1
         state.results[host]["success_ops"] += 1
 
-        _status_log = "Success" if len(op_data["commands"]) > 0 else "No changes"
+        _status_log = "Success" if executed_commands > 0 else "No changes"
         _click_log_status = click.style(_status_log, "green")
         logger.info("{0}{1}".format(host.print_prefix, _click_log_status))
 
@@ -188,7 +188,7 @@ def run_condition(condition_name: str) -> bool:
         if executed_commands:
             state.results[host]["partial_ops"] += 1
 
-        _command_description = f"executed {executed_commands}/{len(op_data['commands'])} commands"
+        _command_description = f"executed {executed_commands} commands"
         log_error_or_warning(host, ignore_errors, _command_description, continue_on_error)
 
         # Always trigger any error handler
diff --git a/pyinfra/api/state.py b/pyinfra/api/state.py
index e639d645d..01f0f8abd 100644
--- a/pyinfra/api/state.py
+++ b/pyinfra/api/state.py
@@ -224,6 +224,10 @@ def to_dict(self):
             "results": self.results,
         }
 
+    def should_diff(self):
+        # TODO: disable diffs if -y/--yes
+        return not self.is_executing
+
     def add_callback_handler(self, handler):
         if not isinstance(handler, BaseStateCallback):
             raise TypeError(
@@ -285,10 +289,13 @@ def get_op_order(self):
     def get_op_meta(self, op_hash: str):
         return self.op_meta[op_hash]
 
-    def get_op_data(self, host: "Host", op_hash: str):
+    def get_meta_for_host(self, host: "Host"):
+        return self.meta[host]
+
+    def get_op_data_for_host(self, host: "Host", op_hash: str):
         return self.ops[host][op_hash]
 
-    def set_op_data(self, host: "Host", op_hash: str, op_data):
+    def set_op_data_for_host(self, host: "Host", op_hash: str, op_data):
         self.ops[host][op_hash] = op_data
 
     def activate_host(self, host: "Host"):