diff --git a/docs/usage.md b/docs/usage.md
index b334ca20..59324f95 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -164,10 +164,6 @@ includes:
 > The included Taskfiles must be using the same schema version the main
 > Taskfile uses.
 
-> Also, for now included Taskfiles can't include other Taskfiles.
-> This was a deliberate decision to keep use and implementation simple.
-> If you disagree, open an GitHub issue and explain your use case. =)
-
 ### Optional includes
 
 Includes marked as optional will allow Task to continue execution as normal if
diff --git a/task.go b/task.go
index ecd3ed35..672d3e60 100644
--- a/task.go
+++ b/task.go
@@ -107,7 +107,12 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
 // Setup setups Executor's internal state
 func (e *Executor) Setup() error {
 	var err error
-	e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
+	e.Taskfile, err = read.Taskfile(&read.ReaderNode{
+		Dir:        e.Dir,
+		Entrypoint: e.Entrypoint,
+		Parent:     nil,
+		Optional:   false,
+	})
 	if err != nil {
 		return err
 	}
diff --git a/task_test.go b/task_test.go
index 9451a664..98fb68ad 100644
--- a/task_test.go
+++ b/task_test.go
@@ -753,6 +753,35 @@ func TestIncludes(t *testing.T) {
 	tt.Run(t)
 }
 
+func TestIncludesMultiLevel(t *testing.T) {
+	tt := fileContentTest{
+		Dir:       "testdata/includes_multi_level",
+		Target:    "default",
+		TrimSpace: true,
+		Files: map[string]string{
+			"called_one.txt":   "one",
+			"called_two.txt":   "two",
+			"called_three.txt": "three",
+		},
+	}
+	tt.Run(t)
+}
+
+func TestIncludeCycle(t *testing.T) {
+	const dir = "testdata/includes_cycle"
+	expectedError := "task: include cycle detected between testdata/includes_cycle/Taskfile.yml <--> testdata/includes_cycle/one/two/Taskfile.yml"
+
+	var buff bytes.Buffer
+	e := task.Executor{
+		Dir:    dir,
+		Stdout: &buff,
+		Stderr: &buff,
+		Silent: true,
+	}
+
+	assert.EqualError(t, e.Setup(), expectedError)
+}
+
 func TestIncorrectVersionIncludes(t *testing.T) {
 	const dir = "testdata/incorrect_includes"
 	expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3"
diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go
index 46e30211..f53a60a9 100644
--- a/taskfile/read/taskfile.go
+++ b/taskfile/read/taskfile.go
@@ -15,8 +15,6 @@ import (
 )
 
 var (
-	// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes
-	ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile")
 	// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
 	ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
 
@@ -28,21 +26,29 @@ var (
 	}
 )
 
+type ReaderNode struct {
+	Dir        string
+	Entrypoint string
+	Optional   bool
+	Parent     *ReaderNode
+}
+
 // Taskfile reads a Taskfile for a given directory
 // Uses current dir when dir is left empty. Uses Taskfile.yml
 // or Taskfile.yaml when entrypoint is left empty
-func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
-	if dir == "" {
+func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
+	if readerNode.Dir == "" {
 		d, err := os.Getwd()
 		if err != nil {
 			return nil, err
 		}
-		dir = d
+		readerNode.Dir = d
 	}
-	path, err := exists(filepath.Join(dir, entrypoint))
+	path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
 	if err != nil {
 		return nil, err
 	}
+	readerNode.Entrypoint = filepath.Base(path)
 
 	t, err := readTaskfile(path)
 	if err != nil {
@@ -74,9 +80,8 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
 			return err
 		}
 		if !filepath.IsAbs(path) {
-			path = filepath.Join(dir, path)
+			path = filepath.Join(readerNode.Dir, path)
 		}
-
 		path, err = exists(path)
 		if err != nil {
 			if includedTask.Optional {
@@ -85,12 +90,23 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
 			return err
 		}
 
-		includedTaskfile, err := readTaskfile(path)
-		if err != nil {
+		includeReaderNode := &ReaderNode{
+			Dir:        filepath.Dir(path),
+			Entrypoint: filepath.Base(path),
+			Parent:     readerNode,
+			Optional:   includedTask.Optional,
+		}
+
+		if err := checkCircularIncludes(includeReaderNode); err != nil {
 			return err
 		}
-		if includedTaskfile.Includes.Len() > 0 {
-			return ErrIncludedTaskfilesCantHaveIncludes
+
+		includedTaskfile, err := Taskfile(includeReaderNode)
+		if err != nil {
+			if includedTask.Optional {
+				return nil
+			}
+			return err
 		}
 
 		if v >= 3.0 && len(includedTaskfile.Dotenv) > 0 {
@@ -100,12 +116,12 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
 		if includedTask.AdvancedImport {
 			for k, v := range includedTaskfile.Vars.Mapping {
 				o := v
-				o.Dir = filepath.Join(dir, includedTask.Dir)
+				o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
 				includedTaskfile.Vars.Mapping[k] = o
 			}
 			for k, v := range includedTaskfile.Env.Mapping {
 				o := v
-				o.Dir = filepath.Join(dir, includedTask.Dir)
+				o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
 				includedTaskfile.Env.Mapping[k] = o
 			}
 
@@ -128,7 +144,7 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
 	}
 
 	if v < 3.0 {
-		path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
+		path = filepath.Join(readerNode.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
 		if _, err = os.Stat(path); err == nil {
 			osTaskfile, err := readTaskfile(path)
 			if err != nil {
@@ -178,3 +194,25 @@ func exists(path string) (string, error) {
 
 	return "", fmt.Errorf(`task: No Taskfile found in "%s". Use "task --init" to create a new one`, path)
 }
+
+func checkCircularIncludes(node *ReaderNode) error {
+	if node == nil {
+		return errors.New("task: failed to check for include cycle: node was nil")
+	}
+	if node.Parent == nil {
+		return errors.New("task: failed to check for include cycle: node.Parent was nil")
+	}
+	var curNode = node
+	var basePath = filepath.Join(node.Dir, node.Entrypoint)
+	for curNode.Parent != nil {
+		curNode = curNode.Parent
+		curPath := filepath.Join(curNode.Dir, curNode.Entrypoint)
+		if curPath == basePath {
+			return fmt.Errorf("task: include cycle detected between %s <--> %s",
+				curPath,
+				filepath.Join(node.Parent.Dir, node.Parent.Entrypoint),
+			)
+		}
+	}
+	return nil
+}
diff --git a/testdata/includes_cycle/Taskfile.yml b/testdata/includes_cycle/Taskfile.yml
new file mode 100644
index 00000000..2eee81b4
--- /dev/null
+++ b/testdata/includes_cycle/Taskfile.yml
@@ -0,0 +1,12 @@
+version: '3'
+
+includes:
+  'one': ./one/Taskfile.yml
+
+tasks:
+  default:
+    cmds:
+      - echo "called_dep" > called_dep.txt
+  level1:
+    cmds:
+      - echo "hello level 1"
diff --git a/testdata/includes_cycle/one/Taskfile.yml b/testdata/includes_cycle/one/Taskfile.yml
new file mode 100644
index 00000000..a8be0fd1
--- /dev/null
+++ b/testdata/includes_cycle/one/Taskfile.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+includes:
+  'two': ./two/Taskfile.yml
+
+tasks:
+  level2:
+    cmds:
+      - echo "hello level 2"
diff --git a/testdata/includes_cycle/one/two/Taskfile.yml b/testdata/includes_cycle/one/two/Taskfile.yml
new file mode 100644
index 00000000..d849448a
--- /dev/null
+++ b/testdata/includes_cycle/one/two/Taskfile.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+includes:
+  bad: "../../Taskfile.yml"
+
+tasks:
+  level3:
+    cmds:
+      - echo "hello level 3"
diff --git a/testdata/includes_multi_level/Taskfile.yml b/testdata/includes_multi_level/Taskfile.yml
new file mode 100644
index 00000000..3734d069
--- /dev/null
+++ b/testdata/includes_multi_level/Taskfile.yml
@@ -0,0 +1,11 @@
+version: '3'
+
+includes:
+  'one': ./one/
+
+tasks:
+  default:
+    cmds:
+      - task: one:default
+      - task: one:two:default
+      - task: one:two:three:default
diff --git a/testdata/includes_multi_level/called_one.txt b/testdata/includes_multi_level/called_one.txt
new file mode 100644
index 00000000..5626abf0
--- /dev/null
+++ b/testdata/includes_multi_level/called_one.txt
@@ -0,0 +1 @@
+one
diff --git a/testdata/includes_multi_level/called_three.txt b/testdata/includes_multi_level/called_three.txt
new file mode 100644
index 00000000..2bdf67ab
--- /dev/null
+++ b/testdata/includes_multi_level/called_three.txt
@@ -0,0 +1 @@
+three
diff --git a/testdata/includes_multi_level/called_two.txt b/testdata/includes_multi_level/called_two.txt
new file mode 100644
index 00000000..f719efd4
--- /dev/null
+++ b/testdata/includes_multi_level/called_two.txt
@@ -0,0 +1 @@
+two
diff --git a/testdata/includes_multi_level/one/Taskfile.yml b/testdata/includes_multi_level/one/Taskfile.yml
new file mode 100644
index 00000000..11ee8f74
--- /dev/null
+++ b/testdata/includes_multi_level/one/Taskfile.yml
@@ -0,0 +1,8 @@
+version: '3'
+
+includes:
+  'two': ./two/
+
+tasks:
+  default: echo one > called_one.txt
+  
\ No newline at end of file
diff --git a/testdata/includes_multi_level/one/two/Taskfile.yml b/testdata/includes_multi_level/one/two/Taskfile.yml
new file mode 100644
index 00000000..ed393064
--- /dev/null
+++ b/testdata/includes_multi_level/one/two/Taskfile.yml
@@ -0,0 +1,7 @@
+version: '3'
+
+includes:
+  'three': ./three/Taskfile.yml
+
+tasks:
+  default: echo two > called_two.txt
diff --git a/testdata/includes_multi_level/one/two/three/Taskfile.yml b/testdata/includes_multi_level/one/two/three/Taskfile.yml
new file mode 100644
index 00000000..b674fbc6
--- /dev/null
+++ b/testdata/includes_multi_level/one/two/three/Taskfile.yml
@@ -0,0 +1,5 @@
+version: '3'
+
+tasks:
+  default: echo three > called_three.txt
+  
\ No newline at end of file