Repository: kurtosis-tech/kurtosis
Pull request #757 (2023-06-22 15:39:14)

feat: Persist enclave plan in the Starlark executor memory
## Description:
The Starlark executor now keeps track of all instructions that were executed within this enclave. It doesn't take into account the manual modification that might have been made, such as manual file uploads via the CLI.

## Is this change user facing?
NO
<!-- If yes, please add the "user facing" label to the PR -->
<!-- If yes, don't forget to include docs changes where relevant -->

## References (if applicable):
<!-- Add relevant Github Issues, Discord threads, or other helpful information. -->



Patch:
diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan.go b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan.go
new file mode 100644
index 000000000..4036c3511
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan.go
@@ -0,0 +1,69 @@
+package instructions_plan
+
+import (
+	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/uuid_generator"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors"
+	"github.com/kurtosis-tech/stacktrace"
+	"go.starlark.net/starlark"
+)
+
+// InstructionsPlan is the object to store a sequence of instructions which forms a "plan" for the enclave.
+// Right now, the object is fairly simple in the sense of it just stores literally the sequence of instructions, and
+// a bit of metadata about each instruction (i.e. whether it has been executed of not, for example)
+// The plan is "append-only", i.e. when an instruction is added, it cannot be removed.
+// The only read method is GeneratePlan unwraps the plan into an actual list of instructions that can be submitted to
+// the executor.
+type InstructionsPlan struct {
+	scheduledInstructionsIndex map[ScheduledInstructionUuid]*ScheduledInstruction
+
+	instructionsSequence []ScheduledInstructionUuid
+}
+
+func NewInstructionsPlan() *InstructionsPlan {
+	return &InstructionsPlan{
+		scheduledInstructionsIndex: map[ScheduledInstructionUuid]*ScheduledInstruction{},
+		instructionsSequence:       []ScheduledInstructionUuid{},
+	}
+}
+
+func (plan *InstructionsPlan) AddInstruction(instruction kurtosis_instruction.KurtosisInstruction, returnedValue starlark.Value) error {
+	generatedUuid, err := uuid_generator.GenerateUUIDString()
+	if err != nil {
+		return stacktrace.Propagate(err, "Unable to generate a random UUID for instruction '%s' to add it to the plan", instruction.String())
+	}
+
+	scheduledInstructionUuid := ScheduledInstructionUuid(generatedUuid)
+	scheduledInstruction := NewScheduledInstruction(scheduledInstructionUuid, instruction, returnedValue)
+
+	plan.scheduledInstructionsIndex[scheduledInstructionUuid] = scheduledInstruction
+	plan.instructionsSequence = append(plan.instructionsSequence, scheduledInstructionUuid)
+	return nil
+}
+
+func (plan *InstructionsPlan) AddScheduledInstruction(scheduledInstruction *ScheduledInstruction) *ScheduledInstruction {
+	newScheduledInstructionUuid := scheduledInstruction.uuid
+	newScheduledInstruction := NewScheduledInstruction(newScheduledInstructionUuid, scheduledInstruction.kurtosisInstruction, scheduledInstruction.returnedValue)
+	newScheduledInstruction.Executed(scheduledInstruction.IsExecuted())
+
+	plan.scheduledInstructionsIndex[newScheduledInstructionUuid] = newScheduledInstruction
+	plan.instructionsSequence = append(plan.instructionsSequence, newScheduledInstructionUuid)
+	return newScheduledInstruction
+}
+
+// GeneratePlan unwraps the plan into a list of instructions
+func (plan *InstructionsPlan) GeneratePlan() ([]*ScheduledInstruction, *startosis_errors.InterpretationError) {
+	var generatedPlan []*ScheduledInstruction
+	for _, instructionUuid := range plan.instructionsSequence {
+		instruction, found := plan.scheduledInstructionsIndex[instructionUuid]
+		if !found {
+			return nil, startosis_errors.NewInterpretationError("Unexpected error generating the Kurtosis Instructions plan. Instruction with UUID '%s' was scheduled but could not be found in Kurtosis instruction index", instructionUuid)
+		}
+		generatedPlan = append(generatedPlan, instruction)
+	}
+	return generatedPlan, nil
+}
+
+func (plan *InstructionsPlan) Size() int {
+	return len(plan.instructionsSequence)
+}
diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction.go b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction.go
new file mode 100644
index 000000000..149da462d
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction.go
@@ -0,0 +1,49 @@
+package instructions_plan
+
+import (
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"go.starlark.net/starlark"
+)
+
+type ScheduledInstructionUuid string
+
+// ScheduledInstruction is a wrapper around a KurtosisInstruction to specify that the instruction is part of an
+// InstructionPlan. The instruction plan can either be the current enclave plan (which has been executed) or a newly
+// generated plan from the latest interpretation.
+// In any case, the ScheduledInstructionUuid stores the result object from the interpretation of the instruction,
+// as well as a flag to track whether this instruction was already executed or not.
+type ScheduledInstruction struct {
+	uuid ScheduledInstructionUuid
+
+	kurtosisInstruction kurtosis_instruction.KurtosisInstruction
+
+	returnedValue starlark.Value
+
+	executed bool
+}
+
+func NewScheduledInstruction(uuid ScheduledInstructionUuid, kurtosisInstruction kurtosis_instruction.KurtosisInstruction, returnedValue starlark.Value) *ScheduledInstruction {
+	return &ScheduledInstruction{
+		uuid:                uuid,
+		kurtosisInstruction: kurtosisInstruction,
+		returnedValue:       returnedValue,
+		executed:            false,
+	}
+}
+
+func (instruction *ScheduledInstruction) GetInstruction() kurtosis_instruction.KurtosisInstruction {
+	return instruction.kurtosisInstruction
+}
+
+func (instruction *ScheduledInstruction) GetReturnedValue() starlark.Value {
+	return instruction.returnedValue
+}
+
+func (instruction *ScheduledInstruction) Executed(isExecuted bool) *ScheduledInstruction {
+	instruction.executed = isExecuted
+	return instruction
+}
+
+func (instruction *ScheduledInstruction) IsExecuted() bool {
+	return instruction.executed
+}
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go
index c7b04cf43..637287cbd 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go
@@ -1,7 +1,7 @@
 package plan_module
 
 import (
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction"
 	"go.starlark.net/starlark"
 	"go.starlark.net/starlarkstruct"
@@ -12,12 +12,12 @@ const (
 )
 
 func PlanModule(
-	instructionsQueue *[]kurtosis_instruction.KurtosisInstruction,
+	instructionsPlan *instructions_plan.InstructionsPlan,
 	kurtosisPlanInstructions []*kurtosis_plan_instruction.KurtosisPlanInstruction,
 ) *starlarkstruct.Module {
 	moduleBuiltins := starlark.StringDict{}
 	for _, planInstruction := range kurtosisPlanInstructions {
-		wrappedPlanInstruction := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(planInstruction, instructionsQueue)
+		wrappedPlanInstruction := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(planInstruction, instructionsPlan)
 		moduleBuiltins[planInstruction.GetName()] = starlark.NewBuiltin(planInstruction.GetName(), wrappedPlanInstruction.CreateBuiltin())
 	}
 
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go
index d50c54c20..55942dd39 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go
@@ -1,8 +1,9 @@
 package kurtosis_plan_instruction
 
 import (
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors"
 	"go.starlark.net/starlark"
 )
 
@@ -20,13 +21,13 @@ type KurtosisPlanInstructionWrapper struct {
 	*KurtosisPlanInstruction
 
 	// TODO: This can be changed to KurtosisPlanInstructionInternal when we get rid of KurtosisInstruction
-	instructionQueue *[]kurtosis_instruction.KurtosisInstruction
+	instructionsPlan *instructions_plan.InstructionsPlan
 }
 
-func NewKurtosisPlanInstructionWrapper(instruction *KurtosisPlanInstruction, instructionQueue *[]kurtosis_instruction.KurtosisInstruction) *KurtosisPlanInstructionWrapper {
+func NewKurtosisPlanInstructionWrapper(instruction *KurtosisPlanInstruction, instructionsPlan *instructions_plan.InstructionsPlan) *KurtosisPlanInstructionWrapper {
 	return &KurtosisPlanInstructionWrapper{
 		KurtosisPlanInstruction: instruction,
-		instructionQueue:        instructionQueue,
+		instructionsPlan:        instructionsPlan,
 	}
 }
 
@@ -44,8 +45,12 @@ func (builtin *KurtosisPlanInstructionWrapper) CreateBuiltin() func(thread *star
 		}
 
 		// before returning, automatically add instruction to queue
-		*builtin.instructionQueue = append(*builtin.instructionQueue, instructionWrapper)
-
+		if err := builtin.instructionsPlan.AddInstruction(instructionWrapper, returnedFutureValue); err != nil {
+			return nil, startosis_errors.WrapWithInterpretationError(err,
+				"Unable to add Kurtosis instruction '%s' at position '%s' to the current plan being assembled. This is a Kurtosis internal bug",
+				instructionWrapper.String(),
+				instructionWrapper.GetPositionInOriginalScript().String())
+		}
 		return returnedFutureValue, nil
 	}
 }
diff --git a/core/server/api_container/server/startosis_engine/startosis_executor.go b/core/server/api_container/server/startosis_engine/startosis_executor.go
index 5a37267b7..2e0355e09 100644
--- a/core/server/api_container/server/startosis_engine/startosis_executor.go
+++ b/core/server/api_container/server/startosis_engine/startosis_executor.go
@@ -4,10 +4,11 @@ import (
 	"context"
 	"github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings"
 	"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/binding_constructors"
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/shared_helpers/magic_string_helper"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/runtime_value_store"
 	"github.com/kurtosis-tech/stacktrace"
+	"github.com/sirupsen/logrus"
 	"sync"
 )
 
@@ -16,8 +17,13 @@ const (
 	ParallelismParam = "PARALLELISM"
 )
 
+var (
+	skippedInstructionOutput = "SKIPPED"
+)
+
 type StartosisExecutor struct {
 	mutex             *sync.Mutex
+	enclavePlan       *instructions_plan.InstructionsPlan
 	runtimeValueStore *runtime_value_store.RuntimeValueStore
 }
 
@@ -28,6 +34,7 @@ type ExecutionError struct {
 func NewStartosisExecutor(runtimeValueStore *runtime_value_store.RuntimeValueStore) *StartosisExecutor {
 	return &StartosisExecutor{
 		mutex:             &sync.Mutex{},
+		enclavePlan:       instructions_plan.NewInstructionsPlan(),
 		runtimeValueStore: runtimeValueStore,
 	}
 }
@@ -39,7 +46,7 @@ func NewStartosisExecutor(runtimeValueStore *runtime_value_store.RuntimeValueSto
 // - A regular KurtosisInstruction that was successfully executed
 // - A KurtosisExecutionError if the execution failed
 // - A ProgressInfo to update the current "state" of the execution
-func (executor *StartosisExecutor) Execute(ctx context.Context, dryRun bool, parallelism int, instructions []kurtosis_instruction.KurtosisInstruction, serializedScriptOutput string) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine {
+func (executor *StartosisExecutor) Execute(ctx context.Context, dryRun bool, parallelism int, instructionsSequence []*instructions_plan.ScheduledInstruction, serializedScriptOutput string) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine {
 	executor.mutex.Lock()
 	starlarkRunResponseLineStream := make(chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine)
 	ctxWithParallelism := context.WithValue(ctx, ParallelismParam, parallelism)
@@ -49,43 +56,59 @@ func (executor *StartosisExecutor) Execute(ctx context.Context, dryRun bool, par
 			close(starlarkRunResponseLineStream)
 		}()
 
-		totalNumberOfInstructions := uint32(len(instructions))
-		for index, instruction := range instructions {
+		// TODO: for now the plan is append only, as each Starlark run happens on top of whatever exists in the enclave
+		logrus.Debugf("Current enclave plan contains %d instuctions. About to process a new plan with %d instructions (dry-run: %v)",
+			executor.enclavePlan.Size(), len(instructionsSequence), dryRun)
+		totalNumberOfInstructions := uint32(len(instructionsSequence))
+		for index, scheduledInstruction := range instructionsSequence {
 			instructionNumber := uint32(index + 1)
 			progress := binding_constructors.NewStarlarkRunResponseLineFromSinglelineProgressInfo(
 				progressMsg, instructionNumber, totalNumberOfInstructions)
 			starlarkRunResponseLineStream <- progress
 
+			instruction := scheduledInstruction.GetInstruction()
 			canonicalInstruction := binding_constructors.NewStarlarkRunResponseLineFromInstruction(instruction.GetCanonicalInstruction())
 			starlarkRunResponseLineStream <- canonicalInstruction
 
 			if !dryRun {
-				instructionOutput, err := instruction.Execute(ctxWithParallelism)
+				var err error
+				var instructionOutput *string
+				if scheduledInstruction.IsExecuted() {
+					// instruction already executed within this enclave. Do not run it
+					instructionOutput = &skippedInstructionOutput
+				} else {
+					instructionOutput, err = instruction.Execute(ctxWithParallelism)
+				}
 				if err != nil {
-
-					propagatedError := stacktrace.Propagate(err, "An error occurred executing instruction (number %d) at %v:\n%v", instructionNumber, instruction.GetPositionInOriginalScript().String(), instruction.String())
-					serializedError := binding_constructors.NewStarlarkExecutionError(propagatedError.Error())
-					starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromExecutionError(serializedError)
-					starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent()
+					sendErrorAndFail(starlarkRunResponseLineStream, err, "An error occurred executing instruction (number %d) at %v:\n%v", instructionNumber, instruction.GetPositionInOriginalScript().String(), instruction.String())
 					return
 				}
 				if instructionOutput != nil {
 					starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromInstructionResult(*instructionOutput)
 				}
+				// mark the instruction as executed and add it to the current instruction plan
+				executor.enclavePlan.AddScheduledInstruction(scheduledInstruction).Executed(true)
 			}
 		}
 
 		if !dryRun {
 			scriptWithValuesReplaced, err := magic_string_helper.ReplaceRuntimeValueInString(serializedScriptOutput, executor.runtimeValueStore)
 			if err != nil {
-				propagatedErr := stacktrace.Propagate(err, "An error occurred while replacing the runtime values in the output of the script")
-				serializedError := binding_constructors.NewStarlarkExecutionError(propagatedErr.Error())
-				starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromExecutionError(serializedError)
-				starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent()
+				sendErrorAndFail(starlarkRunResponseLineStream, err, "An error occurred while replacing the runtime values in the output of the script")
 				return
 			}
 			starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunSuccessEvent(scriptWithValuesReplaced)
+			logrus.Debugf("Current enclave plan has been updated. It now contains %d instructions", executor.enclavePlan.Size())
+		} else {
+			logrus.Debugf("Current enclave plan remained the same as the it was a dry-run. It contains %d instructions", executor.enclavePlan.Size())
 		}
 	}()
 	return starlarkRunResponseLineStream
 }
+
+func sendErrorAndFail(starlarkRunResponseLineStream chan<- *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine, err error, msg string, msgArgs ...interface{}) {
+	propagatedErr := stacktrace.Propagate(err, msg, msgArgs...)
+	serializedError := binding_constructors.NewStarlarkExecutionError(propagatedErr.Error())
+	starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromExecutionError(serializedError)
+	starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent()
+}
diff --git a/core/server/api_container/server/startosis_engine/startosis_interpreter.go b/core/server/api_container/server/startosis_engine/startosis_interpreter.go
index 48e0ffe54..74bcad6a8 100644
--- a/core/server/api_container/server/startosis_engine/startosis_interpreter.go
+++ b/core/server/api_container/server/startosis_engine/startosis_interpreter.go
@@ -7,7 +7,7 @@ import (
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins/print_builtin"
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/package_io"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/runtime_value_store"
@@ -84,17 +84,17 @@ func (interpreter *StartosisInterpreter) Interpret(
 	mainFunctionName string,
 	serializedStarlark string,
 	serializedJsonParams string,
-) (string, []kurtosis_instruction.KurtosisInstruction, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) {
+) (string, *instructions_plan.InstructionsPlan, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) {
 	interpreter.mutex.Lock()
 	defer interpreter.mutex.Unlock()
-	var instructionsQueue []kurtosis_instruction.KurtosisInstruction
+	newInstructionsPlan := instructions_plan.NewInstructionsPlan()
 	logrus.Debugf("Interpreting package '%v' with contents '%v' and params '%v'", packageId, serializedStarlark, serializedJsonParams)
-	globalVariables, interpretationErr := interpreter.interpretInternal(packageId, serializedStarlark, &instructionsQueue)
+	globalVariables, interpretationErr := interpreter.interpretInternal(packageId, serializedStarlark, newInstructionsPlan)
 	if interpretationErr != nil {
 		return startosis_constants.NoOutputObject, nil, interpretationErr.ToAPIType()
 	}
 
-	logrus.Debugf("Successfully interpreted Starlark code into instruction queue: \n%s", instructionsQueue)
+	logrus.Debugf("Successfully interpreted Starlark code into %d instructions", newInstructionsPlan.Size())
 
 	var isUsingDefaultMainFunction bool
 	// if the user sends "" or "run" we isUsingDefaultMainFunction to true
@@ -104,13 +104,13 @@ func (interpreter *StartosisInterpreter) Interpret(
 	}
 
 	if !globalVariables.Has(mainFunctionName) {
-		return missingMainFunctionReturnValue(packageId, mainFunctionName)
+		return "", nil, missingMainFunctionError(packageId, mainFunctionName)
 	}
 
 	mainFunction, ok := globalVariables[mainFunctionName].(*starlark.Function)
-	// if there is a element with the `mainFunctionName` but it isn't a function we have to error as well
+	// if there is an element with the `mainFunctionName` but it isn't a function we have to error as well
 	if !ok {
-		return missingMainFunctionReturnValue(packageId, mainFunctionName)
+		return "", nil, missingMainFunctionError(packageId, mainFunctionName)
 	}
 
 	runFunctionExecutionThread := newStarlarkThread(starlarkGoThreadName)
@@ -129,7 +129,7 @@ func (interpreter *StartosisInterpreter) Interpret(
 		firstParamName, _ := mainFunction.Param(planParamIndex)
 		if firstParamName == planParamName {
 			kurtosisPlanInstructions := KurtosisPlanInstructions(interpreter.serviceNetwork, interpreter.recipeExecutor, interpreter.moduleContentProvider)
-			planModule := plan_module.PlanModule(&instructionsQueue, kurtosisPlanInstructions)
+			planModule := plan_module.PlanModule(newInstructionsPlan, kurtosisPlanInstructions)
 			argsTuple = append(argsTuple, planModule)
 		}
 
@@ -174,17 +174,17 @@ func (interpreter *StartosisInterpreter) Interpret(
 		if interpretationError != nil {
 			return startosis_constants.NoOutputObject, nil, interpretationError.ToAPIType()
 		}
-		return serializedOutputObject, instructionsQueue, nil
+		return serializedOutputObject, newInstructionsPlan, nil
 	}
-	return startosis_constants.NoOutputObject, instructionsQueue, nil
+	return startosis_constants.NoOutputObject, newInstructionsPlan, nil
 }
 
-func (interpreter *StartosisInterpreter) interpretInternal(packageId string, serializedStarlark string, instructionsQueue *[]kurtosis_instruction.KurtosisInstruction) (starlark.StringDict, *startosis_errors.InterpretationError) {
+func (interpreter *StartosisInterpreter) interpretInternal(packageId string, serializedStarlark string, instructionPlan *instructions_plan.InstructionsPlan) (starlark.StringDict, *startosis_errors.InterpretationError) {
 	// We spin up a new thread for every call to interpreterInternal such that the stacktrace provided by the Starlark
 	// Go interpreter is relative to each individual thread, and we don't keep accumulating stacktrace entries from the
 	// previous calls inside the same thread
 	thread := newStarlarkThread(packageId)
-	predeclared, interpretationErr := interpreter.buildBindings(instructionsQueue)
+	predeclared, interpretationErr := interpreter.buildBindings(instructionPlan)
 	if interpretationErr != nil {
 		return nil, interpretationErr
 	}
@@ -197,9 +197,9 @@ func (interpreter *StartosisInterpreter) interpretInternal(packageId string, ser
 	return globalVariables, nil
 }
 
-func (interpreter *StartosisInterpreter) buildBindings(instructionsQueue *[]kurtosis_instruction.KurtosisInstruction) (*starlark.StringDict, *startosis_errors.InterpretationError) {
+func (interpreter *StartosisInterpreter) buildBindings(instructionPlan *instructions_plan.InstructionsPlan) (*starlark.StringDict, *startosis_errors.InterpretationError) {
 	recursiveInterpretForModuleLoading := func(moduleId string, serializedStartosis string) (starlark.StringDict, *startosis_errors.InterpretationError) {
-		result, err := interpreter.interpretInternal(moduleId, serializedStartosis, instructionsQueue)
+		result, err := interpreter.interpretInternal(moduleId, serializedStartosis, instructionPlan)
 		if err != nil {
 			return nil, err
 		}
@@ -305,9 +305,9 @@ func generateInterpretationError(err error) *startosis_errors.InterpretationErro
 	return startosis_errors.NewInterpretationError("UnknownError: %s\n", err.Error())
 }
 
-func missingMainFunctionReturnValue(packageId string, mainFunctionName string) (string, []kurtosis_instruction.KurtosisInstruction, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) {
+func missingMainFunctionError(packageId string, mainFunctionName string) *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError {
 	if packageId == startosis_constants.PackageIdPlaceholderForStandaloneScript {
-		return "", nil, startosis_errors.NewInterpretationError(
+		return startosis_errors.NewInterpretationError(
 			"No '%s' function found in the script; a '%s' entrypoint function with the signature `%s(plan, args)` or `%s()` is required in the Kurtosis script",
 			mainFunctionName,
 			mainFunctionName,
@@ -316,7 +316,7 @@ func missingMainFunctionReturnValue(packageId string, mainFunctionName string) (
 		).ToAPIType()
 	}
 
-	return "", nil, startosis_errors.NewInterpretationError(
+	return startosis_errors.NewInterpretationError(
 		"No '%s' function found in the main file of package '%s'; a '%s' entrypoint function with the signature `%s(plan, args)` or `%s()` is required in the main file of the Kurtosis package",
 		mainFunctionName,
 		packageId,
diff --git a/core/server/api_container/server/startosis_engine/startosis_runner.go b/core/server/api_container/server/startosis_engine/startosis_runner.go
index 2892137fa..51951977d 100644
--- a/core/server/api_container/server/startosis_engine/startosis_runner.go
+++ b/core/server/api_container/server/startosis_engine/startosis_runner.go
@@ -73,22 +73,29 @@ func (runner *StartosisRunner) Run(
 			startingInterpretationMsg, defaultCurrentStepNumber, defaultTotalStepsNumber)
 		starlarkRunResponseLines <- progressInfo
 
-		serializedScriptOutput, instructionsList, interpretationError := runner.startosisInterpreter.Interpret(ctx, packageId, mainFunctionName, serializedStartosis, serializedParams)
+		serializedScriptOutput, instructionsPlan, interpretationError := runner.startosisInterpreter.Interpret(ctx, packageId, mainFunctionName, serializedStartosis, serializedParams)
 		if interpretationError != nil {
 			starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromInterpretationError(interpretationError)
 			starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent()
 			return
 		}
-		totalNumberOfInstructions := uint32(len(instructionsList))
-		logrus.Debugf("Successfully interpreted Starlark script into a series of Kurtosis instructions: \n%v",
-			instructionsList)
+		totalNumberOfInstructions := uint32(instructionsPlan.Size())
+		logrus.Debugf("Successfully interpreted Starlark script into a series of %d Kurtosis instructions",
+			totalNumberOfInstructions)
+
+		instructionsSequence, interpretationErr := instructionsPlan.GeneratePlan()
+		if interpretationErr != nil {
+			starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromInterpretationError(interpretationErr.ToAPIType())
+			starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent()
+			return
+		}
 
 		// Validation starts > send progress info
 		progressInfo = binding_constructors.NewStarlarkRunResponseLineFromSinglelineProgressInfo(
 			startingValidationMsg, defaultCurrentStepNumber, totalNumberOfInstructions)
 		starlarkRunResponseLines <- progressInfo
 
-		validationErrorsChan := runner.startosisValidator.Validate(ctx, instructionsList)
+		validationErrorsChan := runner.startosisValidator.Validate(ctx, instructionsSequence)
 		if isRunFinished := forwardKurtosisResponseLineChannelUntilSourceIsClosed(validationErrorsChan, starlarkRunResponseLines); isRunFinished {
 			return
 		}
@@ -99,11 +106,11 @@ func (runner *StartosisRunner) Run(
 			startingExecutionMsg, defaultCurrentStepNumber, totalNumberOfInstructions)
 		starlarkRunResponseLines <- progressInfo
 
-		executionResponseLinesChan := runner.startosisExecutor.Execute(ctx, dryRun, parallelism, instructionsList, serializedScriptOutput)
+		executionResponseLinesChan := runner.startosisExecutor.Execute(ctx, dryRun, parallelism, instructionsSequence, serializedScriptOutput)
 		if isRunFinished := forwardKurtosisResponseLineChannelUntilSourceIsClosed(executionResponseLinesChan, starlarkRunResponseLines); !isRunFinished {
 			logrus.Warnf("Execution finished but no 'RunFinishedEvent' was received through the stream. This is unexpected as every execution should be terminal.")
 		}
-		logrus.Debugf("Successfully executed the list of %d Kurtosis instructions", len(instructionsList))
+		logrus.Debugf("Successfully executed the list of %d Kurtosis instructions", totalNumberOfInstructions)
 
 	}()
 	return starlarkRunResponseLines
diff --git a/core/server/api_container/server/startosis_engine/startosis_validator.go b/core/server/api_container/server/startosis_engine/startosis_validator.go
index 9070770d3..16d6a8033 100644
--- a/core/server/api_container/server/startosis_engine/startosis_validator.go
+++ b/core/server/api_container/server/startosis_engine/startosis_validator.go
@@ -8,7 +8,7 @@ import (
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network"
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_validator"
 	"github.com/kurtosis-tech/kurtosis/core/server/commons/enclave_data_directory"
@@ -36,7 +36,7 @@ func NewStartosisValidator(kurtosisBackend *backend_interface.KurtosisBackend, s
 	}
 }
 
-func (validator *StartosisValidator) Validate(ctx context.Context, instructions []kurtosis_instruction.KurtosisInstruction) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine {
+func (validator *StartosisValidator) Validate(ctx context.Context, instructionsSequence []*instructions_plan.ScheduledInstruction) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine {
 	starlarkRunResponseLineStream := make(chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine)
 	go func() {
 		defer close(starlarkRunResponseLineStream)
@@ -61,7 +61,7 @@ func (validator *StartosisValidator) Validate(ctx context.Context, instructions
 			serviceNamePortIdMapping)
 
 		isValidationFailure = isValidationFailure ||
-			validator.validateAndUpdateEnvironment(instructions, environment, starlarkRunResponseLineStream)
+			validator.validateAndUpdateEnvironment(instructionsSequence, environment, starlarkRunResponseLineStream)
 		logrus.Debug("Finished validating environment. Validating and downloading container images.")
 
 		isValidationFailure = isValidationFailure ||
@@ -77,12 +77,20 @@ func (validator *StartosisValidator) Validate(ctx context.Context, instructions
 	return starlarkRunResponseLineStream
 }
 
-func (validator *StartosisValidator) validateAndUpdateEnvironment(instructions []kurtosis_instruction.KurtosisInstruction, environment *startosis_validator.ValidatorEnvironment, starlarkRunResponseLineStream chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine) bool {
+func (validator *StartosisValidator) validateAndUpdateEnvironment(instructionsSequence []*instructions_plan.ScheduledInstruction, environment *startosis_validator.ValidatorEnvironment, starlarkRunResponseLineStream chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine) bool {
 	isValidationFailure := false
-	for _, instruction := range instructions {
+	for _, scheduledInstruction := range instructionsSequence {
+		if scheduledInstruction.IsExecuted() {
+			// no need to validate the instruction as it won't be executed in this round
+			continue
+		}
+		instruction := scheduledInstruction.GetInstruction()
 		err := instruction.ValidateAndUpdateEnvironment(environment)
 		if err != nil {
-			wrappedValidationError := startosis_errors.WrapWithValidationError(err, "Error while validating instruction %v. The instruction can be found at %v", instruction.String(), instruction.GetPositionInOriginalScript().String())
+			wrappedValidationError := startosis_errors.WrapWithValidationError(err,
+				"Error while validating instruction %v. The instruction can be found at %v",
+				instruction.String(),
+				instruction.GetPositionInOriginalScript().String())
 			starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromValidationError(wrappedValidationError.ToAPIType())
 			isValidationFailure = true
 		}


Test patch:
diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan_test.go b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan_test.go
new file mode 100644
index 000000000..aa867a895
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan_test.go
@@ -0,0 +1,86 @@
+package instructions_plan
+
+import (
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/mock_instruction"
+	"github.com/stretchr/testify/require"
+	"go.starlark.net/starlark"
+	"testing"
+)
+
+func TestAddInstruction(t *testing.T) {
+	plan := NewInstructionsPlan()
+
+	instruction1 := mock_instruction.NewMockKurtosisInstruction(t)
+	instruction1ReturnedValue := starlark.None
+	require.NoError(t, plan.AddInstruction(instruction1, instruction1ReturnedValue))
+
+	require.Len(t, plan.instructionsSequence, 1)
+	scheduledInstructionUuid := plan.instructionsSequence[0]
+	require.Contains(t, plan.scheduledInstructionsIndex, scheduledInstructionUuid)
+	scheduledInstruction, found := plan.scheduledInstructionsIndex[scheduledInstructionUuid]
+	require.True(t, found)
+	require.Equal(t, scheduledInstruction.GetInstruction(), instruction1)
+	require.False(t, scheduledInstruction.IsExecuted())
+	require.Equal(t, scheduledInstruction.GetReturnedValue(), instruction1ReturnedValue)
+}
+
+func TestAddScheduledInstruction(t *testing.T) {
+	plan := NewInstructionsPlan()
+
+	instruction1Uuid := ScheduledInstructionUuid("instruction1")
+	instruction1 := mock_instruction.NewMockKurtosisInstruction(t)
+	instruction1ReturnedValue := starlark.MakeInt(1)
+	scheduleInstruction := NewScheduledInstruction(instruction1Uuid, instruction1, instruction1ReturnedValue)
+	scheduleInstruction.executed = true
+
+	plan.AddScheduledInstruction(scheduleInstruction)
+
+	require.Len(t, plan.instructionsSequence, 1)
+	scheduledInstructionUuid := plan.instructionsSequence[0]
+	require.Equal(t, instruction1Uuid, scheduledInstructionUuid)
+	require.Contains(t, plan.scheduledInstructionsIndex, scheduledInstructionUuid)
+	addedScheduledInstruction, found := plan.scheduledInstructionsIndex[scheduledInstructionUuid]
+	require.True(t, found)
+	require.NotSame(t, scheduleInstruction, addedScheduledInstruction) // validate the instruction was cloned
+	require.Equal(t, addedScheduledInstruction.GetInstruction(), instruction1)
+	require.True(t, addedScheduledInstruction.IsExecuted())
+	require.Equal(t, addedScheduledInstruction.GetReturnedValue(), instruction1ReturnedValue)
+}
+
+func TestGeneratePlan(t *testing.T) {
+	plan := NewInstructionsPlan()
+
+	// add instruction1 which is marked as executed
+	instruction1Uuid := ScheduledInstructionUuid("instruction1")
+	instruction1 := mock_instruction.NewMockKurtosisInstruction(t)
+	instruction1ReturnedValue := starlark.None
+	scheduleInstruction1 := NewScheduledInstruction(instruction1Uuid, instruction1, instruction1ReturnedValue)
+	scheduleInstruction1.Executed(true)
+
+	plan.scheduledInstructionsIndex[instruction1Uuid] = scheduleInstruction1
+	plan.instructionsSequence = append(plan.instructionsSequence, instruction1Uuid)
+
+	// add instruction2 which by default is not executed
+	instruction2Uuid := ScheduledInstructionUuid("instruction2")
+	instruction2 := mock_instruction.NewMockKurtosisInstruction(t)
+	instruction2ReturnedValue := starlark.MakeInt(1)
+	scheduleInstruction2 := NewScheduledInstruction(instruction2Uuid, instruction2, instruction2ReturnedValue)
+
+	plan.scheduledInstructionsIndex[instruction2Uuid] = scheduleInstruction2
+	plan.instructionsSequence = append(plan.instructionsSequence, instruction2Uuid)
+
+	// generate plan and validate it
+	instructionsSequence, err := plan.GeneratePlan()
+	require.Nil(t, err)
+
+	require.Len(t, instructionsSequence, 2)
+	scheduledInstruction1 := instructionsSequence[0]
+	require.Equal(t, scheduledInstruction1.GetInstruction(), instruction1)
+	require.True(t, scheduledInstruction1.IsExecuted())
+	require.Equal(t, scheduledInstruction1.GetReturnedValue(), instruction1ReturnedValue)
+
+	scheduledInstruction2 := instructionsSequence[1]
+	require.Equal(t, scheduledInstruction2.GetInstruction(), instruction2)
+	require.False(t, scheduledInstruction2.IsExecuted())
+	require.Equal(t, scheduledInstruction2.GetReturnedValue(), instruction2ReturnedValue)
+}
diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction_test.go b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction_test.go
new file mode 100644
index 000000000..597bba176
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction_test.go
@@ -0,0 +1,19 @@
+package instructions_plan
+
+import (
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/mock_instruction"
+	"github.com/stretchr/testify/require"
+	"go.starlark.net/starlark"
+	"testing"
+)
+
+func TestExecutedDefaultAndUpdate(t *testing.T) {
+	instruction1Uuid := ScheduledInstructionUuid("instruction1")
+	instruction1 := mock_instruction.NewMockKurtosisInstruction(t)
+	instruction1ReturnedValue := starlark.MakeInt(1)
+	scheduleInstruction := NewScheduledInstruction(instruction1Uuid, instruction1, instruction1ReturnedValue)
+
+	require.False(t, scheduleInstruction.executed)
+	scheduleInstruction.Executed(true)
+	require.True(t, scheduleInstruction.executed)
+}
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go
index 711d63935..2f11b25d7 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins"
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/builtin_argument"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_constants"
@@ -73,13 +73,13 @@ func TestAllRegisteredBuiltins(t *testing.T) {
 
 func testKurtosisPlanInstruction(t *testing.T, builtin KurtosisPlanInstructionBaseTest) {
 	testId := builtin.GetId()
-	var instructionQueue []kurtosis_instruction.KurtosisInstruction
+	instructionsPlan := instructions_plan.NewInstructionsPlan()
 	thread := newStarlarkThread("framework-testing-engine")
 
 	predeclared := getBasePredeclaredDict(t)
 	// Add the KurtosisPlanInstruction that is being tested
 	instructionFromBuiltin := builtin.GetInstruction()
-	instructionWrapper := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(instructionFromBuiltin, &instructionQueue)
+	instructionWrapper := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(instructionFromBuiltin, instructionsPlan)
 	predeclared[instructionWrapper.GetName()] = starlark.NewBuiltin(instructionWrapper.GetName(), instructionWrapper.CreateBuiltin())
 
 	starlarkCode := builtin.GetStarlarkCode()
@@ -87,8 +87,10 @@ func testKurtosisPlanInstruction(t *testing.T, builtin KurtosisPlanInstructionBa
 	require.Nil(t, err, "Error interpreting Starlark code for instruction '%s'", testId)
 	interpretationResult := extractResultValue(t, globals)
 
-	require.Len(t, instructionQueue, 1)
-	instructionToExecute := instructionQueue[0]
+	require.Equal(t, 1, instructionsPlan.Size())
+	instructionsSequence, err := instructionsPlan.GeneratePlan()
+	require.Nil(t, err)
+	instructionToExecute := instructionsSequence[0].GetInstruction()
 
 	// execute the instruction and run custom builtin assertions
 	executionResult, err := instructionToExecute.Execute(context.WithValue(context.Background(), "PARALLELISM", 1))
diff --git a/core/server/api_container/server/startosis_engine/startosis_executor_test.go b/core/server/api_container/server/startosis_engine/startosis_executor_test.go
index ea9847406..aacd279c3 100644
--- a/core/server/api_container/server/startosis_engine/startosis_executor_test.go
+++ b/core/server/api_container/server/startosis_engine/startosis_executor_test.go
@@ -5,12 +5,13 @@ import (
 	"errors"
 	"github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings"
 	"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/binding_constructors"
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/mock_instruction"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/runtime_value_store"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
+	"go.starlark.net/starlark"
 	"strings"
 	"testing"
 )
@@ -35,14 +36,21 @@ func TestExecuteKurtosisInstructions_ExecuteForReal_Success(t *testing.T) {
 
 	executor := NewStartosisExecutor(runtime_value_store.NewRuntimeValueStore())
 
+	instructionsPlan := instructions_plan.NewInstructionsPlan()
+	instruction0 := createMockInstruction(t, "instruction0", executeSuccessfully)
+	scheduledInstruction0 := instructions_plan.NewScheduledInstruction("instruction0", instruction0, starlark.None).Executed(true)
+	instructionsPlan.AddScheduledInstruction(scheduledInstruction0)
+
 	instruction1 := createMockInstruction(t, "instruction1", executeSuccessfully)
 	instruction2 := createMockInstruction(t, "instruction2", executeSuccessfully)
-	instructions := []kurtosis_instruction.KurtosisInstruction{
-		instruction1,
-		instruction2,
-	}
+	require.NoError(t, instructionsPlan.AddInstruction(instruction1, starlark.None))
+	require.NoError(t, instructionsPlan.AddInstruction(instruction2, starlark.None))
 
-	_, serializedInstruction, err := executeSynchronously(t, executor, executeForReal, instructions)
+	require.Equal(t, executor.enclavePlan.Size(), 0) // check that the enclave plan is empty prior to execution
+
+	_, serializedInstruction, err := executeSynchronously(t, executor, executeForReal, instructionsPlan)
+	instruction0.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1)
+	instruction0.AssertNumberOfCalls(t, "Execute", 0) // not executed as it was already executed
 	instruction1.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1)
 	instruction1.AssertNumberOfCalls(t, "Execute", 1)
 	instruction2.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1)
@@ -51,10 +59,12 @@ func TestExecuteKurtosisInstructions_ExecuteForReal_Success(t *testing.T) {
 	require.Nil(t, err)
 
 	expectedSerializedInstructions := []*kurtosis_core_rpc_api_bindings.StarlarkInstruction{
+		binding_constructors.NewStarlarkInstruction(dummyPosition.ToAPIType(), "instruction0", "instruction0()", noInstructionArgsForTesting),
 		binding_constructors.NewStarlarkInstruction(dummyPosition.ToAPIType(), "instruction1", "instruction1()", noInstructionArgsForTesting),
 		binding_constructors.NewStarlarkInstruction(dummyPosition.ToAPIType(), "instruction2", "instruction2()", noInstructionArgsForTesting),
 	}
 	require.Equal(t, expectedSerializedInstructions, serializedInstruction)
+	require.Equal(t, executor.enclavePlan.Size(), 3) // check that the enclave plan now contains the 3 instructions
 }
 
 func TestExecuteKurtosisInstructions_ExecuteForReal_FailureHalfWay(t *testing.T) {
@@ -63,13 +73,12 @@ func TestExecuteKurtosisInstructions_ExecuteForReal_FailureHalfWay(t *testing.T)
 	instruction1 := createMockInstruction(t, "instruction1", executeSuccessfully)
 	instruction2 := createMockInstruction(t, "instruction2", throwOnExecute)
 	instruction3 := createMockInstruction(t, "instruction3", executeSuccessfully)
-	instructions := []kurtosis_instruction.KurtosisInstruction{
-		instruction1,
-		instruction2,
-		instruction3,
-	}
+	instructionsPlan := instructions_plan.NewInstructionsPlan()
+	require.NoError(t, instructionsPlan.AddInstruction(instruction1, starlark.None))
+	require.NoError(t, instructionsPlan.AddInstruction(instruction2, starlark.None))
+	require.NoError(t, instructionsPlan.AddInstruction(instruction3, starlark.None))
 
-	_, serializedInstruction, executionError := executeSynchronously(t, executor, executeForReal, instructions)
+	_, serializedInstruction, executionError := executeSynchronously(t, executor, executeForReal, instructionsPlan)
 	instruction1.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1)
 	instruction1.AssertNumberOfCalls(t, "Execute", 1)
 	instruction2.AssertNumberOfCalls(t, "String", 1)
@@ -99,12 +108,11 @@ func TestExecuteKurtosisInstructions_DoDryRun(t *testing.T) {
 
 	instruction1 := createMockInstruction(t, "instruction1", executeSuccessfully)
 	instruction2 := createMockInstruction(t, "instruction2", executeSuccessfully)
-	instructions := []kurtosis_instruction.KurtosisInstruction{
-		instruction1,
-		instruction2,
-	}
+	instructionsPlan := instructions_plan.NewInstructionsPlan()
+	require.NoError(t, instructionsPlan.AddInstruction(instruction1, starlark.None))
+	require.NoError(t, instructionsPlan.AddInstruction(instruction2, starlark.None))
 
-	_, serializedInstruction, err := executeSynchronously(t, executor, doDryRun, instructions)
+	_, serializedInstruction, err := executeSynchronously(t, executor, doDryRun, instructionsPlan)
 	instruction1.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1)
 	instruction2.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1)
 	// both execute never called because dry run = true
@@ -139,11 +147,14 @@ func createMockInstruction(t *testing.T, instructionName string, executeSuccessf
 	return instruction
 }
 
-func executeSynchronously(t *testing.T, executor *StartosisExecutor, dryRun bool, instructions []kurtosis_instruction.KurtosisInstruction) (string, []*kurtosis_core_rpc_api_bindings.StarlarkInstruction, *kurtosis_core_rpc_api_bindings.StarlarkExecutionError) {
+func executeSynchronously(t *testing.T, executor *StartosisExecutor, dryRun bool, instructionsPlan *instructions_plan.InstructionsPlan) (string, []*kurtosis_core_rpc_api_bindings.StarlarkInstruction, *kurtosis_core_rpc_api_bindings.StarlarkExecutionError) {
 	scriptOutput := strings.Builder{}
 	var serializedInstructions []*kurtosis_core_rpc_api_bindings.StarlarkInstruction
 
-	executionResponseLines := executor.Execute(context.Background(), dryRun, noParallelism, instructions, noScriptOutputObject)
+	scheduledInstructions, err := instructionsPlan.GeneratePlan()
+	require.Nil(t, err)
+
+	executionResponseLines := executor.Execute(context.Background(), dryRun, noParallelism, scheduledInstructions, noScriptOutputObject)
 	for executionResponseLine := range executionResponseLines {
 		if executionResponseLine.GetError() != nil {
 			return scriptOutput.String(), serializedInstructions, executionResponseLine.GetError().GetExecutionError()
diff --git a/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go b/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go
index 291b9283e..f64193ce8 100644
--- a/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go
+++ b/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins/print_builtin"
-	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/kurtosis_print"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/remove_service"
@@ -49,13 +49,13 @@ def run(plan):
 	plan.print("` + testString + `")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 1) // Only the print statement
+	require.Equal(t, 1, instructionsPlan.Size()) // Only the print statement
 
 	expectedOutput := testString + `
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_RandomMainFunctionAndParamsWithPlan(t *testing.T) {
@@ -76,14 +76,14 @@ def deploy_contract(plan,service_name,contract_name,init_message,args):
 	mainFunctionName := "deploy_contract"
 	inputArgs := `{"service_name": "my-service", "contract_name": "my-contract", "init_message": "Init message", "args": {"arg1": "arg1-value", "arg2": "arg2-value"}}`
 
-	result, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs)
+	result, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 3) // The three print functions
+	require.Equal(t, 3, instructionsPlan.Size()) // The three print functions
 	require.NotNil(t, result)
 	expectedResult := "\"arg1-value:arg2-value\""
 	require.Equal(t, expectedResult, result)
 	expectedOutput := "Service name: service_name\nContract name: contract_name\nInit message: init_message\n"
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_RandomMainFunctionAndParams(t *testing.T) {
@@ -103,9 +103,9 @@ def my_func(my_arg1, my_arg2, args):
 	mainFunctionName := "my_func"
 	inputArgs := `{"my_arg1": "foo", "my_arg2": "bar", "args": {"arg1": "arg1-value", "arg2": "arg2-value"}}`
 
-	result, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs)
+	result, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 0) // There are no instructions to execute
+	require.Equal(t, 0, instructionsPlan.Size()) // There are no instructions to execute
 	require.NotNil(t, result)
 	expectedResult := "\"foo--bar--arg1-value:arg2-value\""
 	require.Equal(t, expectedResult, result)
@@ -125,14 +125,14 @@ def run(plan):
 	plan.print(my_dict)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Len(t, instructions, 2) // Only the print statement
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
+	require.Equal(t, 2, instructionsPlan.Size())
 
 	expectedOutput := `{}
 {"hello": "world"}
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ScriptFailingSingleError(t *testing.T) {
@@ -147,8 +147,7 @@ def run(plan):
 unknownInstruction()
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 
 	expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg(
 		[]startosis_errors.CallFrame{
@@ -157,6 +156,7 @@ unknownInstruction()
 		multipleInterpretationErrorMsg,
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_ScriptFailingMultipleErrors(t *testing.T) {
@@ -172,8 +172,7 @@ unknownVariable
 unknownInstruction2()
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 
 	expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg(
 		[]startosis_errors.CallFrame{
@@ -184,6 +183,7 @@ unknownInstruction2()
 		multipleInterpretationErrorMsg,
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_ScriptFailingSyntaxError(t *testing.T) {
@@ -198,8 +198,7 @@ def run():
 load("otherScript.start") # fails b/c load takes in at least 2 args
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 
 	expectedError := startosis_errors.NewInterpretationErrorFromStacktrace(
 		[]startosis_errors.CallFrame{
@@ -207,6 +206,7 @@ load("otherScript.start") # fails b/c load takes in at least 2 args
 		},
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_ValidSimpleScriptWithInstruction(t *testing.T) {
@@ -234,18 +234,18 @@ def run(plan):
 	plan.print("The grpc transport protocol is " + datastore_service.ports["grpc"].transport_protocol)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 5)
+	require.Equal(t, 5, instructionsPlan.Size())
 
-	assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 15, 38)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 15, 38)
 
 	expectedOutput := `Starting Startosis script!
 Adding service example-datastore-server
 The grpc port is 1323
 The grpc transport protocol is TCP
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ValidSimpleScriptWithApplicationProtocol(t *testing.T) {
@@ -273,9 +273,9 @@ def run(plan):
 	plan.print("The transport protocol is " + datastore_service.ports["grpc"].transport_protocol)
 	plan.print("The application protocol is " + datastore_service.ports["grpc"].application_protocol)
 `
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 6)
+	require.Equal(t, 6, instructionsPlan.Size())
 
 	expectedOutput := `Starting Startosis script!
 Adding service example-datastore-server
@@ -283,7 +283,7 @@ The port is 1323
 The transport protocol is TCP
 The application protocol is http
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ValidSimpleScriptWithInstructionMissingContainerName(t *testing.T) {
@@ -307,8 +307,7 @@ def run(plan):
 	plan.add_service(name = service_name, config = config)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 
 	expectedError := startosis_errors.NewInterpretationErrorWithCauseAndCustomMsg(
 		errors.New("ServiceConfig: missing argument for image"),
@@ -319,6 +318,7 @@ def run(plan):
 		"Evaluation error: Cannot construct 'ServiceConfig' from the provided arguments.",
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_ValidSimpleScriptWithInstructionTypoInProtocol(t *testing.T) {
@@ -342,8 +342,7 @@ def run(plan):
 	plan.add_service(name = service_name, config = config)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	expectedError := startosis_errors.NewInterpretationErrorWithCauseAndCustomMsg(
 		startosis_errors.NewInterpretationError(`The following argument(s) could not be parsed or did not pass validation: {"transport_protocol":"Invalid argument value for 'transport_protocol': 'TCPK'. Valid values are TCP, SCTP, UDP"}`),
 		[]startosis_errors.CallFrame{
@@ -353,6 +352,7 @@ def run(plan):
 		"Evaluation error: Cannot construct 'PortSpec' from the provided arguments.",
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_ValidSimpleScriptWithInstructionPortNumberAsString(t *testing.T) {
@@ -376,8 +376,7 @@ def run(plan):
 	plan.add_service(name = service_name, config = config)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	expectedError := startosis_errors.NewInterpretationErrorWithCauseAndCustomMsg(
 		startosis_errors.NewInterpretationError(`The following argument(s) could not be parsed or did not pass validation: {"number":"Value for 'number' was expected to be an integer between 1 and 65535, but it was 'starlark.String'"}`),
 		[]startosis_errors.CallFrame{
@@ -387,6 +386,7 @@ def run(plan):
 		"Evaluation error: Cannot construct 'PortSpec' from the provided arguments.",
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_ValidScriptWithMultipleInstructions(t *testing.T) {
@@ -420,13 +420,13 @@ def run(plan):
 	plan.print("Done!")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 8)
+	require.Equal(t, 8, instructionsPlan.Size())
 
-	assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19)
-	assertInstructionTypeAndPosition(t, instructions[4], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19)
-	assertInstructionTypeAndPosition(t, instructions[6], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 4, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 6, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19)
 
 	expectedOutput := `Starting Startosis script!
 Adding service example-datastore-server-0
@@ -434,7 +434,7 @@ Adding service example-datastore-server-1
 Adding service example-datastore-server-2
 Done!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_LoadStatementIsDisallowedInKurtosis(t *testing.T) {
@@ -452,7 +452,7 @@ load("` + barModulePath + `", "a")
 def run(plan):
 	plan.print("Hello " + a)
 `
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg(
 		[]startosis_errors.CallFrame{
 			*startosis_errors.NewCallFrame("<toplevel>", startosis_errors.NewScriptPosition(startosis_constants.PackageIdPlaceholderForStandaloneScript, 2, 1)),
@@ -461,7 +461,7 @@ def run(plan):
 	).ToAPIType()
 
 	require.Equal(t, expectedError, interpretationError)
-	require.Empty(t, instructions)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_SimpleImport(t *testing.T) {
@@ -479,13 +479,13 @@ my_module = import_module("` + barModulePath + `")
 def run(plan):
 	plan.print("Hello " + my_module.a)
 `
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Len(t, instructions, 1) // Only the print statement
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
+	require.Equal(t, 1, instructionsPlan.Size()) // Only the print statement
 
 	expectedOutput := `Hello World!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_TransitiveLoading(t *testing.T) {
@@ -508,13 +508,13 @@ def run(plan):
 
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Len(t, instructions, 1) // Only the print statement
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
+	require.Equal(t, 1, instructionsPlan.Size())
 
 	expectedOutput := `Hello World!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_FailsOnCycle(t *testing.T) {
@@ -536,8 +536,7 @@ def run(plan):
 	plan.print(module_doo.b)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions) // No kurtosis instruction
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg(
 		[]startosis_errors.CallFrame{
 			*startosis_errors.NewCallFrame("<toplevel>", startosis_errors.NewScriptPosition(moduleBarLoadsModuleDoo, 1, 27)),
@@ -547,6 +546,7 @@ def run(plan):
 		"Evaluation error: There's a cycle in the import_module calls",
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_FailsOnNonExistentModule(t *testing.T) {
@@ -561,8 +561,7 @@ def run(plan):
 	plan.print(my_module.b)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Empty(t, instructions) // No kurtosis instruction
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 
 	errorMsg := `Evaluation error: An error occurred while loading the module '` + nonExistentModule + `'
 	Caused by: Package '` + nonExistentModule + `' not found`
@@ -573,6 +572,7 @@ def run(plan):
 		errorMsg,
 	).ToAPIType()
 	require.Equal(t, expectedError, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_ImportingAValidModuleThatPreviouslyFailedToLoadSucceeds(t *testing.T) {
@@ -588,19 +588,19 @@ def run(plan):
 `
 
 	// assert that first load fails
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Nil(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.NotNil(t, interpretationError)
+	require.Nil(t, instructionsPlan)
 
 	barModuleContents := "a=\"World!\""
 	require.Nil(t, packageContentProvider.AddFileContent(barModulePath, barModuleContents))
 	expectedOutput := `Hello World!
 `
 	// assert that second load succeeds
-	_, instructions, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 1) // The print statement
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	require.Equal(t, 1, instructionsPlan.Size())
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ValidSimpleScriptWithImportedStruct(t *testing.T) {
@@ -628,16 +628,16 @@ def run(plan):
 	plan.add_service(name = module_bar.service_name, config = module_bar.config)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Len(t, instructions, 3)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
+	require.Equal(t, 3, instructionsPlan.Size())
 
-	assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 6, 18)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 6, 18)
 
 	expectedOutput := `Starting Startosis script!
 Adding service example-datastore-server
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ValidScriptWithFunctionsImportedFromOtherModule(t *testing.T) {
@@ -676,13 +676,13 @@ def run(plan):
 	plan.print("Done!")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 8)
+	require.Equal(t, 8, instructionsPlan.Size())
 
-	assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, moduleBar, 18, 25)
-	assertInstructionTypeAndPosition(t, instructions[4], add_service.AddServiceBuiltinName, moduleBar, 18, 25)
-	assertInstructionTypeAndPosition(t, instructions[6], add_service.AddServiceBuiltinName, moduleBar, 18, 25)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, moduleBar, 18, 25)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 4, add_service.AddServiceBuiltinName, moduleBar, 18, 25)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 6, add_service.AddServiceBuiltinName, moduleBar, 18, 25)
 
 	expectedOutput := `Starting Startosis script!
 Adding service example-datastore-server-0
@@ -690,7 +690,7 @@ Adding service example-datastore-server-1
 Adding service example-datastore-server-2
 Done!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ImportModuleWithNoGlobalVariables(t *testing.T) {
@@ -709,13 +709,13 @@ def run(plan):
 	plan.print("World!")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Len(t, instructions, 1)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
+	require.Equal(t, 1, instructionsPlan.Size())
 
 	expectedOutput := `World!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_TestInstructionQueueAndOutputBufferDontHaveDupesInterpretingAnotherScript(t *testing.T) {
@@ -751,11 +751,11 @@ Adding service example-datastore-server
 Starting Startosis script!
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptA, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptA, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 4)
-	assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, moduleBar, 12, 18)
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutputFromScriptA)
+	require.Equal(t, 4, instructionsPlan.Size())
+	assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, moduleBar, 12, 18)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutputFromScriptA)
 
 	scriptB := `
 def run(plan):
@@ -776,11 +776,11 @@ def run(plan):
 Adding service example-datastore-server
 `
 
-	_, instructions, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptB, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptB, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 3)
-	assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 14, 18)
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutputFromScriptB)
+	require.Equal(t, 3, instructionsPlan.Size())
+	assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 14, 18)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutputFromScriptB)
 }
 
 func TestStartosisInterpreter_ReadFileFromGithub(t *testing.T) {
@@ -800,14 +800,14 @@ def run(plan):
 	plan.print(file_contents)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 2)
+	require.Equal(t, 2, instructionsPlan.Size())
 
 	expectedOutput := `Reading file from GitHub!
 this is a test string
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_RenderTemplates(t *testing.T) {
@@ -838,16 +838,16 @@ def run(plan):
 	plan.print(artifact_name)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 3)
+	require.Equal(t, 3, instructionsPlan.Size())
 
-	assertInstructionTypeAndPosition(t, instructions[1], render_templates.RenderTemplatesBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 20, 39)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 1, render_templates.RenderTemplatesBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 20, 39)
 
 	expectedOutput := fmt.Sprintf(`Rendering template to disk!
 %v
 `, testArtifactName)
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ThreeLevelNestedInstructionPositionTest(t *testing.T) {
@@ -884,17 +884,17 @@ def run(plan):
 	plan.print(uuid)
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 4)
+	require.Equal(t, 4, instructionsPlan.Size())
 
-	assertInstructionTypeAndPosition(t, instructions[2], store_service_files.StoreServiceFilesBuiltinName, storeFileDefinitionPath, 4, 40)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 2, store_service_files.StoreServiceFilesBuiltinName, storeFileDefinitionPath, 4, 40)
 
 	expectedOutput := fmt.Sprintf(`In the module that calls store.star
 In the store files instruction
 %v
 `, testArtifactName)
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_ValidSimpleRemoveService(t *testing.T) {
@@ -910,16 +910,16 @@ def run(plan):
 	plan.print("The service example-datastore-server has been removed")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Len(t, instructions, 3)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
+	require.Equal(t, 3, instructionsPlan.Size())
 
-	assertInstructionTypeAndPosition(t, instructions[1], remove_service.RemoveServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 5, 21)
+	assertInstructionTypeAndPosition(t, instructionsPlan, 1, remove_service.RemoveServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 5, 21)
 
 	expectedOutput := `Starting Startosis script!
 The service example-datastore-server has been removed
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_NoPanicIfUploadIsPassedAPathNotOnDisk(t *testing.T) {
@@ -932,9 +932,9 @@ func TestStartosisInterpreter_NoPanicIfUploadIsPassedAPathNotOnDisk(t *testing.T
 def run(plan):
 	plan.upload_files("` + filePath + `")
 `
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
-	require.Nil(t, instructions)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.NotNil(t, interpretationError)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_RunWithoutArgsNoArgsPassed(t *testing.T) {
@@ -947,13 +947,13 @@ def run(plan):
 	plan.print("Hello World!")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 1)
+	require.Equal(t, 1, instructionsPlan.Size())
 
 	expectedOutput := `Hello World!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_RunWithoutArgsArgsPassed(t *testing.T) {
@@ -966,13 +966,13 @@ def run(plan):
 	plan.print("Hello World!")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 1)
+	require.Equal(t, 1, instructionsPlan.Size())
 
 	expectedOutput := `Hello World!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_RunWithArgsArgsPassed(t *testing.T) {
@@ -985,13 +985,13 @@ def run(plan, args):
 	plan.print("My favorite number is {0}".format(args["number"]))
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 1)
+	require.Equal(t, 1, instructionsPlan.Size())
 
 	expectedOutput := `My favorite number is 4
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_RunWithArgsNoArgsPassed(t *testing.T) {
@@ -1007,13 +1007,13 @@ def run(plan, args):
 		plan.print("Sorry no args!")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.Nil(t, interpretationError)
-	require.Len(t, instructions, 1)
+	require.Equal(t, 1, instructionsPlan.Size())
 
 	expectedOutput := `Sorry no args!
 `
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput)
 }
 
 func TestStartosisInterpreter_RunWithMoreThanExpectedParams(t *testing.T) {
@@ -1026,13 +1026,11 @@ def run(plan, args, invalid_arg):
 	plan.print("this wouldn't interpret so the text here doesnt matter")
 `
 
-	_, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
+	_, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs)
 	require.NotNil(t, interpretationError)
 	expectedError := fmt.Sprintf("The 'run' entrypoint function can have at most '%v' argument got '%v'", maximumParamsAllowedForRunFunction, 3)
 	require.Equal(t, expectedError, interpretationError.GetErrorMessage())
-
-	expectedOutput := ``
-	validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput)
+	require.Nil(t, instructionsPlan)
 }
 
 func TestStartosisInterpreter_PrintWithoutPlanErrorsNicely(t *testing.T) {
@@ -1055,10 +1053,15 @@ def run(plan):
 //	TEST HELPERS
 //
 // #####################################################################################################################
-func validateScriptOutputFromPrintInstructions(t *testing.T, instructions []kurtosis_instruction.KurtosisInstruction, expectedOutput string) {
+func validateScriptOutputFromPrintInstructions(t *testing.T, instructionsPlan *instructions_plan.InstructionsPlan, expectedOutput string) {
 	scriptOutput := strings.Builder{}
-	for _, instruction := range instructions {
-
+	scheduledInstructions, err := instructionsPlan.GeneratePlan()
+	require.Nil(t, err)
+	for _, scheduledInstruction := range scheduledInstructions {
+		if scheduledInstruction.IsExecuted() {
+			continue
+		}
+		instruction := scheduledInstruction.GetInstruction()
 		switch instruction.GetCanonicalInstruction().InstructionName {
 		case kurtosis_print.PrintBuiltinName:
 			instructionOutput, err := instruction.Execute(context.Background())
@@ -1072,7 +1075,11 @@ func validateScriptOutputFromPrintInstructions(t *testing.T, instructions []kurt
 	require.Equal(t, expectedOutput, scriptOutput.String())
 }
 
-func assertInstructionTypeAndPosition(t *testing.T, instruction kurtosis_instruction.KurtosisInstruction, expectedInstructionName string, filename string, expectedLine int32, expectedCol int32) {
+func assertInstructionTypeAndPosition(t *testing.T, instructionsPlan *instructions_plan.InstructionsPlan, idxInPlan int, expectedInstructionName string, filename string, expectedLine int32, expectedCol int32) {
+	scheduledInstructions, err := instructionsPlan.GeneratePlan()
+	require.Nil(t, err)
+	instruction := scheduledInstructions[idxInPlan].GetInstruction()
+
 	canonicalInstruction := instruction.GetCanonicalInstruction()
 	require.Equal(t, expectedInstructionName, canonicalInstruction.GetInstructionName())
 	expectedPosition := binding_constructors.NewStarlarkInstructionPosition(filename, expectedLine, expectedCol)