diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md
index 3ea6a080c5..49340c6c82 100644
--- a/NEXT_CHANGELOG.md
+++ b/NEXT_CHANGELOG.md
@@ -9,5 +9,6 @@
 ### CLI
 
 ### Bundles
+* Fix reading dashboard contents when the sync root is different than the bundle root ([#3006](https://github.com/databricks/cli/pull/3006))
 
 ### API Changes
diff --git a/acceptance/bundle/debug/out.stderr.txt b/acceptance/bundle/debug/out.stderr.txt
index ea082fac30..60de4430e0 100644
--- a/acceptance/bundle/debug/out.stderr.txt
+++ b/acceptance/bundle/debug/out.stderr.txt
@@ -44,6 +44,7 @@
 10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources
 10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources mutator=ResolveVariableReferences(resources)
 10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources mutator=NormalizePaths
+10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources mutator=TranslatePaths
 10:07:59 Debug: Apply pid=12345 mutator=PythonMutator(load)
 10:07:59 Debug: Apply pid=12345 mutator=PythonMutator(init)
 10:07:59 Debug: Apply pid=12345 mutator=PythonMutator(load_resources)
diff --git a/acceptance/bundle/deploy/dashboard/nested-folders/databricks.yml.tmpl b/acceptance/bundle/deploy/dashboard/nested-folders/databricks.yml.tmpl
index f257a2f8df..87620709a3 100644
--- a/acceptance/bundle/deploy/dashboard/nested-folders/databricks.yml.tmpl
+++ b/acceptance/bundle/deploy/dashboard/nested-folders/databricks.yml.tmpl
@@ -1,3 +1,8 @@
+#
+# Acceptance test for deploying dashboards with the following setup:
+# 1. dashboard file is inside the bundle root, but in a separate file from the .yml file
+# 2. sync root is the same as the bundle root
+#
 bundle:
   name: deploy-dashboard-nested-folders-$UNIQUE_NAME
 
diff --git a/acceptance/bundle/deploy/dashboard/simple/databricks.yml.tmpl b/acceptance/bundle/deploy/dashboard/simple/databricks.yml.tmpl
index 27b425c98a..682da0320b 100644
--- a/acceptance/bundle/deploy/dashboard/simple/databricks.yml.tmpl
+++ b/acceptance/bundle/deploy/dashboard/simple/databricks.yml.tmpl
@@ -1,3 +1,8 @@
+#
+# Acceptance test for deploying dashboards with the following setup:
+# 1. dashboard file is inside the bundle root
+# 2. sync root is the same as the bundle root
+#
 bundle:
   name: deploy-dashboard-test-$UNIQUE_NAME
 
diff --git a/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/databricks.yml.tmpl b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/databricks.yml.tmpl
new file mode 100644
index 0000000000..6b3de54b08
--- /dev/null
+++ b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/databricks.yml.tmpl
@@ -0,0 +1,22 @@
+#
+# Acceptance test for deploying dashboards with the following setup:
+# 1. dashboard file is outside the bundle root
+# 2. sync root is one level above bundle root
+#
+bundle:
+  name: deploy-dashboard-outside-bundle-root-$UNIQUE_NAME
+
+sync:
+  paths:
+    - ..
+  include:
+    - ..
+
+resources:
+  dashboards:
+    dashboard1:
+      display_name: $DASHBOARD_DISPLAY_NAME
+      warehouse_id: $TEST_DEFAULT_WAREHOUSE_ID
+      embed_credentials: true
+      file_path: ../sample-dashboard.lvdash.json
+      parent_path: /Users/$CURRENT_USER_NAME
diff --git a/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/output.txt b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/output.txt
new file mode 100644
index 0000000000..df710ff2e3
--- /dev/null
+++ b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/output.txt
@@ -0,0 +1,23 @@
+
+>>> [CLI] bundle deploy
+Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-outside-bundle-root-[UNIQUE_NAME]/default/files...
+Deploying resources...
+Updating deployment state...
+Deployment complete!
+
+>>> [CLI] lakeview get [DASHBOARD_ID]
+{
+  "lifecycle_state": "ACTIVE",
+  "parent_path": "/Users/[USERNAME]",
+  "path": "/Users/[USERNAME]/test bundle-deploy-dashboard [UUID].lvdash.json",
+  "serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}"
+}
+
+>>> [CLI] bundle destroy --auto-approve
+The following resources will be deleted:
+  delete dashboard dashboard1
+
+All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-outside-bundle-root-[UNIQUE_NAME]/default
+
+Deleting files...
+Destroy complete!
diff --git a/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/script b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/script
new file mode 100644
index 0000000000..873d374fbb
--- /dev/null
+++ b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/script
@@ -0,0 +1,18 @@
+DASHBOARD_DISPLAY_NAME="test bundle-deploy-dashboard $(uuid)"
+if [ -z "$CLOUD_ENV" ]; then
+    DASHBOARD_DISPLAY_NAME="test bundle/deploy/ 6260d50f-e8ff-4905-8f28-812345678903"   # use hard-coded uuid when running locally
+    export TEST_DEFAULT_WAREHOUSE_ID="warehouse-1234"
+fi
+cp $TESTDIR/../simple/sample-dashboard.lvdash.json ../.
+
+export DASHBOARD_DISPLAY_NAME
+envsubst < databricks.yml.tmpl > databricks.yml
+
+cleanup() {
+    trace $CLI bundle destroy --auto-approve
+}
+trap cleanup EXIT
+
+trace $CLI bundle deploy
+DASHBOARD_ID=$($CLI bundle summary --output json | jq -r '.resources.dashboards.dashboard1.id')
+trace $CLI lakeview get $DASHBOARD_ID | jq '{lifecycle_state, parent_path, path, serialized_dashboard}'
diff --git a/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/test.toml b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/test.toml
new file mode 100644
index 0000000000..406b398663
--- /dev/null
+++ b/acceptance/bundle/deploy/dashboard/simple_outside_bundle_root/test.toml
@@ -0,0 +1,44 @@
+Local = true
+Cloud = true
+RequiresWarehouse = true
+
+Ignore = [
+    "databricks.yml",
+    "sample-dashboard.lvdash.json",
+]
+
+[[Repls]]
+Old = "[0-9a-f]{32}"
+New = "[DASHBOARD_ID]"
+
+[[Repls]]
+# Windows:
+Old = 'The system cannot find the file specified.'
+New = 'no such file or directory'
+
+[[Server]]
+Pattern = "POST /api/2.0/lakeview/dashboards"
+Response.Body = '''
+{
+    "dashboard_id":"1234567890abcdef1234567890abcdef"
+}
+'''
+
+[[Server]]
+Pattern = "POST /api/2.0/lakeview/dashboards/{dashboard_id}/published"
+
+[[Server]]
+Pattern = "GET /api/2.0/lakeview/dashboards/{dashboard_id}"
+Response.Body = '''
+{
+    "dashboard_id":"1234567890abcdef1234567890abcdef",
+    "display_name": "test dashboard 6260d50f-e8ff-4905-8f28-812345678903",
+    "lifecycle_state": "ACTIVE",
+    "path": "/Users/[USERNAME]/test bundle-deploy-dashboard 6260d50f-e8ff-4905-8f28-812345678903.lvdash.json",
+    "parent_path": "/Users/tester@databricks.com",
+    "serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}"
+}
+'''
+
+[[Server]]
+Pattern = "DELETE /api/2.0/lakeview/dashboards/{dashboard_id}"
diff --git a/acceptance/bundle/deploy/dashboard/simple_syncroot/databricks.yml.tmpl b/acceptance/bundle/deploy/dashboard/simple_syncroot/databricks.yml.tmpl
index 833da56a6b..de130f6672 100644
--- a/acceptance/bundle/deploy/dashboard/simple_syncroot/databricks.yml.tmpl
+++ b/acceptance/bundle/deploy/dashboard/simple_syncroot/databricks.yml.tmpl
@@ -1,3 +1,8 @@
+#
+# Acceptance test for deploying dashboards with the following setup:
+# 1. dashboard file is within the bundle root
+# 2. sync root is one level above bundle root
+#
 bundle:
   name: deploy-dashboard-test-$UNIQUE_NAME
 
diff --git a/acceptance/bundle/deploy/dashboard/simple_syncroot/output.txt b/acceptance/bundle/deploy/dashboard/simple_syncroot/output.txt
index cd0f46dc7a..e9ff6d057b 100644
--- a/acceptance/bundle/deploy/dashboard/simple_syncroot/output.txt
+++ b/acceptance/bundle/deploy/dashboard/simple_syncroot/output.txt
@@ -1,9 +1,23 @@
 
 >>> [CLI] bundle deploy
-Error: failed to read serialized dashboard from file_path sample-dashboard.lvdash.json: open sample-dashboard.lvdash.json: no such file or directory
+Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-test-[UNIQUE_NAME]/default/files...
+Deploying resources...
+Updating deployment state...
+Deployment complete!
 
+>>> [CLI] lakeview get [DASHBOARD_ID]
+{
+  "lifecycle_state": "ACTIVE",
+  "parent_path": "/Users/[USERNAME]",
+  "path": "/Users/[USERNAME]/test bundle-deploy-dashboard [UUID].lvdash.json",
+  "serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}"
+}
 
 >>> [CLI] bundle destroy --auto-approve
-Error: failed to read serialized dashboard from file_path sample-dashboard.lvdash.json: open sample-dashboard.lvdash.json: no such file or directory
+The following resources will be deleted:
+  delete dashboard dashboard1
 
-Exit code: 1
+All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-test-[UNIQUE_NAME]/default
+
+Deleting files...
+Destroy complete!
diff --git a/acceptance/bundle/deploy/dashboard/simple_syncroot/test.toml b/acceptance/bundle/deploy/dashboard/simple_syncroot/test.toml
index 329f76ca71..406b398663 100644
--- a/acceptance/bundle/deploy/dashboard/simple_syncroot/test.toml
+++ b/acceptance/bundle/deploy/dashboard/simple_syncroot/test.toml
@@ -1,4 +1,3 @@
-Badness = "Cannot read dashboard, expecting deployment to succeed"
 Local = true
 Cloud = true
 RequiresWarehouse = true
diff --git a/bundle/config/mutator/resourcemutator/process_static_resources.go b/bundle/config/mutator/resourcemutator/process_static_resources.go
index cef71c62b3..f227bcfdf4 100644
--- a/bundle/config/mutator/resourcemutator/process_static_resources.go
+++ b/bundle/config/mutator/resourcemutator/process_static_resources.go
@@ -49,6 +49,11 @@ func (p processStaticResources) Apply(ctx context.Context, b *bundle.Bundle) dia
 			"variables",
 		),
 		mutator.NormalizePaths(),
+
+		// Translate dashboard paths into paths in the workspace file system
+		// This must occur after NormalizePaths but before NormalizeAndInitializeResources,
+		// since the latter reads dashboard files and requires fully resolved paths.
+		mutator.TranslatePathsDashboards(),
 	)
 	if diags.HasError() {
 		return diags
diff --git a/bundle/config/mutator/translate_paths.go b/bundle/config/mutator/translate_paths.go
index ad707636df..1625719c1a 100644
--- a/bundle/config/mutator/translate_paths.go
+++ b/bundle/config/mutator/translate_paths.go
@@ -47,13 +47,22 @@ func (err ErrIsNotNotebook) Error() string {
 	return fmt.Sprintf("file at %s is not a notebook", err.path)
 }
 
-type translatePaths struct{}
+type translatePaths struct {
+	dashboardsOnly bool
+}
 
 // TranslatePaths converts paths to local notebook files into paths in the workspace file system.
 func TranslatePaths() bundle.Mutator {
 	return &translatePaths{}
 }
 
+// TranslatePathsDashboards converts paths to local dashboard files into paths in the workspace file system.
+func TranslatePathsDashboards() bundle.Mutator {
+	return &translatePaths{
+		dashboardsOnly: true,
+	}
+}
+
 func (m *translatePaths) Name() string {
 	return "TranslatePaths"
 }
@@ -297,13 +306,21 @@ func (m *translatePaths) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn
 
 	err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
 		var err error
-		for _, fn := range []func(context.Context, dyn.Value) (dyn.Value, error){
+
+		translations := []func(context.Context, dyn.Value) (dyn.Value, error){
 			t.applyJobTranslations,
 			t.applyPipelineTranslations,
 			t.applyArtifactTranslations,
-			t.applyDashboardTranslations,
 			t.applyAppsTranslations,
-		} {
+		}
+
+		if m.dashboardsOnly {
+			translations = []func(context.Context, dyn.Value) (dyn.Value, error){
+				t.applyDashboardTranslations,
+			}
+		}
+
+		for _, fn := range translations {
 			v, err = fn(ctx, v)
 			if err != nil {
 				return dyn.InvalidValue, err
