diff --git a/go.mod b/go.mod
index ff64a675821c8034bf9f089a8f2960232ba551c5..566e99fb4100d8110b596b47618e99840b61bd06 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/stretchr/testify v1.9.0
 	github.com/testcontainers/testcontainers-go v0.31.0
 	github.com/testcontainers/testcontainers-go/modules/mockserver v0.31.0
+	gonum.org/v1/gonum v0.15.0
 )
 
 require (
@@ -59,7 +60,7 @@ require (
 	golang.org/x/crypto v0.22.0 // indirect
 	golang.org/x/mod v0.16.0 // indirect
 	golang.org/x/sys v0.19.0 // indirect
-	golang.org/x/tools v0.13.0 // indirect
+	golang.org/x/tools v0.15.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect
 	google.golang.org/grpc v1.58.3 // indirect
 	google.golang.org/protobuf v1.33.0 // indirect
diff --git a/go.sum b/go.sum
index ac34cc9267709ba357bb2915d2d656ceb0a05d95..3235f796ca93d3d205485acd5e6ebf5a4fa72b5f 100644
--- a/go.sum
+++ b/go.sum
@@ -147,6 +147,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
 golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
+golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
@@ -160,8 +162,8 @@ golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -186,12 +188,14 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
-golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
+golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
+gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=
 google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
 google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
 google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
diff --git a/snok.go b/snok.go
index e49d2ddaaa8e82b5dc72457b21148287dcc70444..5400ba5aa4d7f5b2e60776b87d2775134ff14052 100644
--- a/snok.go
+++ b/snok.go
@@ -8,8 +8,6 @@ import (
 	"io"
 	"os"
 	"regexp"
-	"slices"
-	"strconv"
 	"strings"
 	"sync"
 	"testing"
@@ -19,110 +17,123 @@ import (
 	"github.com/spf13/pflag"
 	"github.com/stretchr/testify/require"
 	"github.com/testcontainers/testcontainers-go"
+	"gonum.org/v1/gonum/graph"
+	"gonum.org/v1/gonum/graph/simple"
+	"gonum.org/v1/gonum/graph/topo"
 )
 
-type test struct {
-	Name        string   `json:"name,omitempty"`
-	Args        []string `json:"args,omitempty"`
-	Input       *string  `json:"input,omitempty"`
-	Output      *string  `json:"output,omitempty"`
-	ExpectError bool     `json:"expectError,omitempty"`
+type Tests struct {
+	t             *testing.T
+	RootCmd       *cobra.Command
+	Debug         bool
+	Trace         bool
+	graph         *simple.DirectedGraph
+	unexpectedErr func(require.TestingT, error, ...interface{})
 }
 
-type container struct {
-	startFn func(*CommandTest, *testing.T) error
-	running bool
-}
-
-type CommandTest struct {
-	t                     *testing.T
-	RootCmd               *cobra.Command
-	Debug                 bool
-	Trace                 bool
-	containers            map[string]container
-	handleUnexpectedError func(require.TestingT, error, ...interface{})
-}
-
-func NewCommandTest(cmd *cobra.Command) *CommandTest {
-	ct := &CommandTest{
-		RootCmd:               cmd,
-		Debug:                 false,
-		Trace:                 false,
-		containers:            make(map[string]container),
-		handleUnexpectedError: require.NoError,
+func NewTestSuite(cmd *cobra.Command) *Tests {
+	ct := &Tests{
+		RootCmd:       cmd,
+		Debug:         false,
+		Trace:         false,
+		graph:         simple.NewDirectedGraph(),
+		unexpectedErr: require.NoError,
 	}
-	fmt.Println("parse flags")
 	flag.BoolVar(&ct.Debug, "debug", false, "debug")
 	flag.BoolVar(&ct.Trace, "trace", false, "trace")
 	flag.Parse()
 	return ct
 }
 
-func (ct *CommandTest) AddContainer(name string, startFn func(*CommandTest, *testing.T) error) {
-	ct.containers[name] = container{startFn: startFn}
+func (ct *Tests) GetTest(name string) (*test, error) {
+	node, new := ct.graph.NodeWithID(hash(name))
+	if new {
+		return nil, fmt.Errorf("Undefined test: %s", name)
+	}
+	if test, ok := node.(*test); ok {
+		return test, nil
+	}
+	return nil, fmt.Errorf("Node type is not test: %s", name)
 }
 
-func (ct *CommandTest) Accept(log testcontainers.Log) {
+func (ct *Tests) Accept(log testcontainers.Log) {
 	ct.t.Helper()
 	ct.t.Logf("%s", log.Content)
 }
 
-func (ct *CommandTest) Run(t *testing.T) {
-	ct.t = t
-	ct.testCmd(t, ct.RootCmd, []string{})
+func (tests *Tests) AddHelper(name string, fn func(*Tests, *testing.T) error, needs ...string) {
+	tests.graph.AddNode(&helper{
+		node: node{
+			Name:  name,
+			Needs: needs,
+		},
+		fn: fn,
+	})
 }
 
-func (ct *CommandTest) testCmd(t *testing.T, cmd *cobra.Command, args []string) {
-	args = append(args, cmd.Name())
-	containers, exists := cmd.Annotations["containers"]
-	if exists {
-		ct.startContainers(t, containers)
-	}
+func (tests *Tests) addTests(t *testing.T, cmd *cobra.Command, cmds []string) {
+	cmds = append(cmds, cmd.Name())
 	annotation, exists := cmd.Annotations["tests"]
 	if exists {
-		t.Run(cmd.Name(), func(t *testing.T) {
-			tests := []test{}
-			err := json.Unmarshal([]byte(annotation), &tests)
-			require.NoError(t, err, "Malformed annotation: %s", annotation)
-			for _, test := range tests {
-				ct.executeTest(t, test, args)
-				cmd.Flags().VisitAll(func(pf *pflag.Flag) {
-					if pf.Changed {
-						err := pf.Value.Set(pf.DefValue)
-						require.NoError(t, err, "Error setting %d to %d", pf.Name, pf.DefValue)
-						pf.Changed = false
-					}
-				})
-			}
-		})
+		nodes := []test{}
+		err := json.Unmarshal([]byte(annotation), &nodes)
+		require.NoError(t, err, "Malformed annotation: %s", annotation)
+		for _, test := range nodes {
+			test.cmd = cmd
+			test.Args = append(cmds[1:], test.Args...)
+			tests.graph.AddNode(&test)
+		}
 	}
 	if cmd.HasSubCommands() {
-		// Todo: Use a DAG to determine the order of the tests
-		slices.SortFunc(cmd.Commands(), func(a, b *cobra.Command) int {
-			return getOrder(a) - getOrder(b)
-		})
 		for _, cmd := range cmd.Commands() {
-			ct.testCmd(t, cmd, args)
+			tests.addTests(t, cmd, cmds)
 		}
 	}
 }
 
-func (ct *CommandTest) startContainers(t *testing.T, containers string) {
-	for _, container := range strings.Split(containers, ",") {
-		key := strings.TrimFunc(container, unicode.IsSpace)
-		if c, ok := ct.containers[key]; ok {
-			if !c.running {
-				t.Run("[Start "+key+"]", func(_ *testing.T) {
-					err := c.startFn(ct, t)
-					require.NoError(t, err, "Failed to start container: %s", key)
-				})
-				c.running = true
+func (tests *Tests) calculateExecutionOrder(t *testing.T) []graph.Node {
+	for _, n := range graph.NodesOf(tests.graph.Nodes()) {
+		if node, ok := n.(Node); ok {
+			for _, id := range node.DependencyIDs() {
+				dependency, new := tests.graph.NodeWithID(id)
+				require.False(t, new, "Unknown dependency for %s", node)
+				tests.graph.SetEdge(simple.Edge{F: dependency, T: node})
 			}
 		}
 	}
+	cycles := topo.DirectedCyclesIn(tests.graph)
+	require.Empty(t, cycles, "Detected %d cycles", len(cycles))
+	sorted, err := topo.Sort(tests.graph)
+	require.NoError(t, err)
+	return sorted
 }
 
-func (ct *CommandTest) executeTest(t *testing.T, test test, args []string) {
+func (tests *Tests) Run(t *testing.T) {
+	tests.t = t
+	tests.addTests(t, tests.RootCmd, []string{})
+	nodes := tests.calculateExecutionOrder(t)
+	for _, node := range nodes {
+		if helper, ok := node.(*helper); ok {
+			err := helper.fn(tests, t)
+			require.NoError(t, err, "Failed to execute helper: %s", helper)
+		}
+		if test, ok := node.(*test); ok {
+			tests.executeTest(t, test)
+		}
+	}
+}
+
+func resetFlags(t *testing.T, cmd *cobra.Command) {
+	cmd.Flags().VisitAll(func(pf *pflag.Flag) {
+		if pf.Changed {
+			err := pf.Value.Set(pf.DefValue)
+			require.NoError(t, err, "Error setting %d to %d", pf.Name, pf.DefValue)
+			pf.Changed = false
+		}
+	})
+}
+
+func (tests *Tests) executeTest(t *testing.T, test *test) {
 	t.Run(test.Name, func(t *testing.T) {
 		var input io.Reader
 		if test.Input != nil {
@@ -133,11 +144,12 @@ func (ct *CommandTest) executeTest(t *testing.T, test test, args []string) {
 				}
 			}()
 		}
-		output, err := ct.executeCmd(append(args[1:], test.Args...), input)
+		output, err := tests.executeCmd(test.Args, input)
+		resetFlags(t, test.cmd)
 		if test.ExpectError {
 			require.Error(t, err, "Expected error")
 		} else if err != nil {
-			ct.handleUnexpectedError(t, err, "Unexpected error")
+			tests.unexpectedErr(t, err, "Unexpected error")
 		} else if test.Output != nil {
 			output = strings.TrimRightFunc(output, unicode.IsSpace)
 			if strings.HasPrefix(*test.Output, "/") && strings.HasSuffix(*test.Output, "/") {
@@ -151,7 +163,7 @@ func (ct *CommandTest) executeTest(t *testing.T, test test, args []string) {
 	})
 }
 
-func (ct *CommandTest) executeCmd(args []string, input io.Reader) (string, error) {
+func (tests *Tests) executeCmd(args []string, input io.Reader) (string, error) {
 
 	osStdout := os.Stdout                   // keep backup of the real stdout
 	defer func() { os.Stdout = osStdout }() // restore the real stdout
@@ -164,14 +176,14 @@ func (ct *CommandTest) executeCmd(args []string, input io.Reader) (string, error
 	wg.Add(1)
 	go func() {
 		_, err := io.Copy(output, r)
-		require.NoError(ct.t, err, "IO Error")
+		require.NoError(tests.t, err, "IO Error")
 		wg.Done()
 	}()
 
-	ct.RootCmd.SetIn(input)
-	ct.RootCmd.SetOutput(w)
-	ct.RootCmd.SetArgs(args)
-	err := ct.RootCmd.Execute()
+	tests.RootCmd.SetIn(input)
+	tests.RootCmd.SetOutput(w)
+	tests.RootCmd.SetArgs(args)
+	err := tests.RootCmd.Execute()
 
 	w.Close()
 	wg.Wait()
@@ -179,14 +191,3 @@ func (ct *CommandTest) executeCmd(args []string, input io.Reader) (string, error
 	return output.String(), err
 
 }
-
-func getOrder(cmd *cobra.Command) int {
-	orderStr, exists := cmd.Annotations["order"]
-	if exists {
-		order, err := strconv.Atoi(orderStr)
-		if err == nil {
-			return order
-		}
-	}
-	return 0
-}
diff --git a/snok_test.go b/snok_test.go
index 273b8251ba242fc789eb5f8a024f893f2399605d..cccc774f587a384bc90c4710acb7a8244dac9bd1 100644
--- a/snok_test.go
+++ b/snok_test.go
@@ -16,7 +16,7 @@ import (
 	"github.com/testcontainers/testcontainers-go/modules/mockserver"
 )
 
-var ct *CommandTest
+var tests *Tests
 
 func mockUnexpectedError(t require.TestingT, err error, msgAndArgs ...interface{}) {
 	if err.Error() != "expect the unexpected" {
@@ -28,22 +28,23 @@ func TestMain(m *testing.M) {
 	rootCmd.AddCommand(envCmd, echoCmd, errorCmd)
 	rootCmd.PersistentFlags().BoolP("debug", "d", false, "debug flag")
 	echoCmd.Flags().BoolP("reverse", "r", false, "reverse the output")
-	ct = NewCommandTest(rootCmd)
-	ct.handleUnexpectedError = mockUnexpectedError
-	ct.AddContainer("Test", mockContainer)
+	tests = NewTestSuite(rootCmd)
+	tests.unexpectedErr = mockUnexpectedError
+	tests.AddHelper("TestContainer", mockContainer)
+	tests.AddHelper("PatchURL", patchTestInput, "TestContainer")
 	os.Exit(m.Run())
 }
 
 func TestCmds(t *testing.T) {
-	ct.Run(t)
+	tests.Run(t)
 }
 
-func mockContainer(ct *CommandTest, t *testing.T) (err error) {
+func mockContainer(tests *Tests, t *testing.T) error {
 	if os.Getenv("MOCKSERVER_URL") == "" {
 		ctx := context.Background()
 		opts := []testcontainers.ContainerCustomizer{
 			testcontainers.WithImage("mockserver/mockserver:5.15.0"),
-			testcontainers.WithLogConsumers(ct),
+			testcontainers.WithLogConsumers(tests),
 		}
 		mockserverContainer, err := mockserver.RunContainer(ctx, opts...)
 		if err != nil && strings.Contains(err.Error(), "Cannot connect to the Docker daemon") {
@@ -56,9 +57,21 @@ func mockContainer(ct *CommandTest, t *testing.T) (err error) {
 		url, err := mockserverContainer.URL(ctx)
 		require.NoError(t, err)
 		t.Setenv("MOCKSERVER_URL", url)
-		echoCmd.Annotations["tests"] = strings.ReplaceAll(echoCmd.Annotations["tests"], "http://localhost", url)
 	}
-	return
+	return nil
+}
+
+func patchTestInput(tests *Tests, t *testing.T) error {
+	_, err := tests.GetTest("TestContainer")
+	require.Error(t, err)
+	_, err = tests.GetTest("Does not exist")
+	require.Error(t, err)
+	test, err := tests.GetTest("Echo Input (http)")
+	dashboardUrl := os.Getenv("MOCKSERVER_URL") + "/mockserver/dashboard"
+	t.Logf("Modifying %s", test.String())
+	test.Input = &dashboardUrl
+	require.NoError(t, err)
+	return nil
 }
 
 var rootCmd = &cobra.Command{
@@ -91,6 +104,7 @@ var envCmd = &cobra.Command{
 		"tests": `[
 			{
 				"name": "Check Container URL",
+				"needs": ["TestContainer"],
 				"args": ["MOCKSERVER_URL"],
 				"output": "/^http:\\/\\/\\S+:\\d+$/"
 			}
@@ -141,12 +155,14 @@ var echoCmd = &cobra.Command{
 			},
 			{
 				"name": "Echo Input (http)",
+				"needs": ["PatchURL"],
 				"args": ["test"],
 				"input": "http://localhost/mockserver/dashboard",
 				"output": "/^test <!doctype html><html lang=\"en\">.*<\\/html>/"
 			},
 			{
-				"name": "Echo Input (ftp)",
+				"name": "Echo Input (string)",
+				"needs": ["Expected Error"],
 				"args": ["test"],
 				"input": "Hello, World!",
 				"output": "test Hello, World!"
@@ -180,6 +196,7 @@ var errorCmd = &cobra.Command{
 		"tests": `[
 			{
 				"name": "Expected Error",
+				"needs": ["Echo Input (http)","Version"],
 				"args": ["hello","world"],
 				"expectError": true
 			},
diff --git a/types.go b/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..c64efe60f256d1ddb15c5ae880c691d65bd417a0
--- /dev/null
+++ b/types.go
@@ -0,0 +1,67 @@
+package snok
+
+import (
+	"hash/fnv"
+	"testing"
+
+	"github.com/spf13/cobra"
+	"gonum.org/v1/gonum/graph"
+)
+
+// hash calculates the hash value of a string using the FNV-1a algorithm.
+func hash(s string) int64 {
+	hash := fnv.New64a()
+	hash.Write([]byte(s))
+	return int64(hash.Sum64())
+}
+
+// Node is a graph node. It returns a graph-unique integer ID and a list of
+// integer IDs of nodes that it depends on.
+type Node interface {
+	graph.Node
+	// DependencyIDs returns a list of graph-unique integer ID for each dependency.
+	DependencyIDs() []int64
+}
+
+// node in the graph. helper and test inherit from this.
+type node struct {
+	id    *int64
+	Name  string   `json:"name,omitempty"`
+	Needs []string `json:"needs,omitempty"`
+}
+
+func (n *node) ID() int64 {
+	if n.id == nil {
+		n.id = new(int64)
+		*n.id = hash(n.Name)
+	}
+	return *n.id
+}
+
+func (n *node) DependencyIDs() []int64 {
+	needs := make([]int64, len(n.Needs))
+	for i, need := range n.Needs {
+		needs[i] = hash(need)
+	}
+	return needs
+}
+
+func (n *node) String() string {
+	return n.Name
+}
+
+// helper node in the graph.
+type helper struct {
+	node
+	fn func(*Tests, *testing.T) error
+}
+
+// test node in the graph.
+type test struct {
+	node
+	cmd         *cobra.Command
+	Args        []string `json:"args,omitempty"`
+	Input       *string  `json:"input,omitempty"`
+	Output      *string  `json:"output,omitempty"`
+	ExpectError bool     `json:"expectError,omitempty"`
+}