Repository: honeycombio/terraform-provider-honeycombio
Pull request #716 (2025-07-11 16:44:34)

feat(r/flexible_board): allow panel positions to be autogenerated
## Which problem is this PR solving?

- changes internals of position on flexible board panel to handle computed values better. 
- This allows the API to compute height, width and coordinates (x,y)
- Without this change, we cannot solve the problem of auto assigning panels to specific grid spots

## How to verify that this has the expected result
- create a flexible board with panels without positions
- create a flexible board with panels with positions

fixes https://github.com/honeycombio/terraform-provider-honeycombio/issues/692 



Patch:
diff --git a/client/board.go b/client/board.go
index 4e92dfe..8c48ded 100644
--- a/client/board.go
+++ b/client/board.go
@@ -44,6 +44,10 @@ type Board struct {
 	// Defaults to "classic".
 	BoardType BoardType `json:"type,omitempty"`
 
+	// Layout generation controls how the board layout is generated.
+	// Defaults to "manual".
+	LayoutGeneration LayoutGeneration `json:"layout_generation,omitempty"`
+
 	// Board panels are the individual panels that make up a board. Each panel can
 	// be a query or an SLO panel. The panels are laid out in a grid.
 	Panels []BoardPanel `json:"panels,omitempty"`
@@ -80,6 +84,10 @@ type BoardPanel struct {
 	SLOPanel   *BoardSLOPanel   `json:"slo_panel,omitempty"`
 }
 
+func (b *BoardPanel) IsBlank() bool {
+	return b.PanelPosition.X == 0 && b.PanelPosition.Y == 0 && b.PanelPosition.Height == 0 && b.PanelPosition.Width == 0
+}
+
 type BoardPanelType string
 
 const (
@@ -128,6 +136,13 @@ const (
 	BoardTypeFlexible BoardType = "flexible"
 )
 
+type LayoutGeneration string
+
+const (
+	LayoutGenerationManual LayoutGeneration = "manual"
+	LayoutGenerationAuto   LayoutGeneration = "auto"
+)
+
 // BoardStyle determines how a Board should be displayed within the Honeycomb UI.
 type BoardStyle string
 
diff --git a/docs/resources/board.md b/docs/resources/board.md
index 7b9e671..1a21cbd 100644
--- a/docs/resources/board.md
+++ b/docs/resources/board.md
@@ -1,10 +1,10 @@
 # Resource: honeycombio_board
 
-Creates a classic board. Classic boards are deprecated, use [honeycombio_flexible_board](./flexible_board.md) resource instead. For more information about boards, check out [Create Custom Boards](https://docs.honeycomb.io/observe/boards).
+Creates a legacy board. Legacy boards are deprecated, use [honeycombio_flexible_board](./flexible_board.md) resource instead. For more information about boards, check out [Create Custom Boards](https://docs.honeycomb.io/observe/boards).
 
 ## Example Usage
 
-### Simple Classic Board
+### Simple Legacy Board
 
 ```hcl
 data "honeycombio_query_specification" "query" {
@@ -35,7 +35,7 @@ resource "honeycombio_board" "board" {
 }
 ```
 
-### Classic Board with a Service Level Objective (SLO) and an annotated query
+### Legacy Board with a Service Level Objective (SLO) and an annotated query
 
 ```hcl
 data "honeycombio_query_specification" "latency_by_userid" {
@@ -116,7 +116,6 @@ Each board configuration may have zero or more `query` blocks, which accept the
 - `graph_settings` - (Optional) A map of boolean toggles to manages the settings for this query's graph on the board.
   If a value is unspecified, it is assumed to be false.
   Currently supported toggles are:
-
     - `hide_markers`
     - `log_scale`
     - `omit_missing_values`
diff --git a/docs/resources/flexible_board.md b/docs/resources/flexible_board.md
index 44fb6cb..3e7beb4 100644
--- a/docs/resources/flexible_board.md
+++ b/docs/resources/flexible_board.md
@@ -90,13 +90,6 @@ resource "honeycombio_flexible_board" "overview" {
   panel {
     type = "query"
 
-    position {
-      x_coordinate = 0
-      y_coordinate = 0
-      width        = 6
-      height       = 6
-    }
-
     query_panel {
       query_id            = honeycombio_query.latency_by_userid.id
       query_annotation_id = honeycombio_query_annotation.latency_by_userid.id
@@ -139,10 +132,10 @@ Each board configuration may have zero or more `panel` blocks which accept the f
 
 Each `position` configuration accepts the following arguments:
 
-- `x_coordinate` - (Optional) The x-axis origin point for placing the panel within the layout.
-- `y_coordinate` - (Optional) The y-axis origin point for placing the panel within the layout.
-- `width` - (Optional) The width of the panel in honeycomb UI columns. Defaults to 6 for queries and 3 for slos. Maximum value is 12.
-- `height` - (Optional) The height of the panel in rows. Defaults to 4.
+- `x_coordinate` - (Optional) The x-axis origin point for placing the panel within the layout. Must be provided with `y_coordinate`.
+- `y_coordinate` - (Optional) The y-axis origin point for placing the panel within the layout. Must be provided with `x_coordinate`.
+- `width` - (Optional) The width of the panel in honeycomb UI columns. Automatically calculated when not provided. Maximum value is 12.
+- `height` - (Optional) The height of the panel in rows. Automatically calculated when not provided.
 
 Each `slo_panel` configuration accepts the following arguments:
 
diff --git a/example/flexible_board/main.tf b/example/flexible_board/main.tf
index 6c4e5a5..e7df349 100644
--- a/example/flexible_board/main.tf
+++ b/example/flexible_board/main.tf
@@ -111,5 +111,11 @@ resource "honeycombio_flexible_board" "overview" {
     slo_panel {
       slo_id = honeycombio_slo.slo.id
     }
+    position {
+      x_coordinate = 6
+      y_coordinate = 0
+      width        = 6
+      height       = 6
+    }
   }
 }
\ No newline at end of file
diff --git a/example/board/main.tf b/example/legacy_board/main.tf
similarity index 100%
rename from example/board/main.tf
rename to example/legacy_board/main.tf
diff --git a/example/board_via_query_result/main.tf b/example/legacy_board_via_query_result/main.tf
similarity index 100%
rename from example/board_via_query_result/main.tf
rename to example/legacy_board_via_query_result/main.tf
diff --git a/example/board_with_annotations/main.tf b/example/legacy_board_with_annotations/main.tf
similarity index 100%
rename from example/board_with_annotations/main.tf
rename to example/legacy_board_with_annotations/main.tf
diff --git a/internal/helper/validation/panel_positions_consistency.go b/internal/helper/validation/panel_positions_consistency.go
new file mode 100644
index 0000000..3761f13
--- /dev/null
+++ b/internal/helper/validation/panel_positions_consistency.go
@@ -0,0 +1,103 @@
+package validation
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ validator.List = panelPositionsConsistencyValidator{}
+
+// panelPositionsConsistencyValidator validates that either all panels have positions or none of them do.
+type panelPositionsConsistencyValidator struct{}
+
+func (v panelPositionsConsistencyValidator) Description(_ context.Context) string {
+	return "either all panels must have positions or none of them should have positions"
+}
+
+func (v panelPositionsConsistencyValidator) MarkdownDescription(ctx context.Context) string {
+	return v.Description(ctx)
+}
+
+func (v panelPositionsConsistencyValidator) ValidateList(ctx context.Context, request validator.ListRequest, response *validator.ListResponse) {
+	if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
+		return
+	}
+
+	elements := request.ConfigValue.Elements()
+	if len(elements) == 0 {
+		return
+	}
+
+	var hasPositionCount int
+	var noPositionCount int
+
+	for _, element := range elements {
+		if element.IsNull() || element.IsUnknown() {
+			continue
+		}
+
+		panelObj, ok := element.(types.Object)
+		if !ok {
+			continue
+		}
+
+		attrs := panelObj.Attributes()
+		position, exists := attrs["position"]
+		if !exists {
+			continue
+		}
+
+		// Check if position is set (not null and not unknown)
+		if position.IsNull() || position.IsUnknown() {
+			noPositionCount++
+		} else {
+			// Check if position object has any actual values set
+			positionObj, ok := position.(types.Object)
+			if !ok {
+				noPositionCount++
+				continue
+			}
+
+			positionAttrs := positionObj.Attributes()
+			hasAnyCoordinate := false
+
+			// Check if any coordinate is set
+			for _, coordName := range []string{"x_coordinate", "y_coordinate"} {
+				if coord, exists := positionAttrs[coordName]; exists {
+					if !coord.IsNull() && !coord.IsUnknown() {
+						hasAnyCoordinate = true
+						break
+					}
+				}
+			}
+
+			if hasAnyCoordinate {
+				hasPositionCount++
+			} else {
+				noPositionCount++
+			}
+		}
+	}
+
+	// If we have both panels with positions and panels without positions, that's invalid
+	if hasPositionCount > 0 && noPositionCount > 0 {
+		response.Diagnostics.Append(
+			validatordiag.InvalidAttributeValueDiagnostic(
+				request.Path,
+				v.Description(ctx),
+				"Found panels with positions and panels without positions. All panels must have consistent position configuration.",
+			),
+		)
+	}
+}
+
+// RequireConsistentPanelPositions returns a ListValidator which ensures that either all panels
+// have positions or none of them do.
+//
+// Null (unconfigured) and unknown (known after apply) values are skipped.
+func RequireConsistentPanelPositions() validator.List {
+	return panelPositionsConsistencyValidator{}
+}
diff --git a/internal/models/flexible_board.go b/internal/models/flexible_board.go
index bd28c10..52651bf 100644
--- a/internal/models/flexible_board.go
+++ b/internal/models/flexible_board.go
@@ -16,17 +16,15 @@ type FlexibleBoardResourceModel struct {
 
 type BoardPanelModel struct {
 	PanelType  types.String `tfsdk:"type"`
-	Position   types.List   `tfsdk:"position"`
+	Position   types.Object `tfsdk:"position"`
 	QueryPanel types.List   `tfsdk:"query_panel"`
 	SLOPanel   types.List   `tfsdk:"slo_panel"`
 }
 
 var BoardPanelModelAttrType = map[string]attr.Type{
 	"type": types.StringType,
-	"position": types.ListType{
-		ElemType: types.ObjectType{
-			AttrTypes: BoardPanelPositionModelAttrType,
-		},
+	"position": types.ObjectType{
+		AttrTypes: BoardPanelPositionModelAttrType,
 	},
 	"query_panel": types.ListType{
 		ElemType: types.ObjectType{
@@ -115,3 +113,13 @@ var ChartSettingsModelAttrType = map[string]attr.Type{
 	"omit_missing_values": types.BoolType,
 	"use_log_scale":       types.BoolType,
 }
+
+// This is the old version of the BoardPanelModel
+// It was used to store the board panel in a list of objects
+// This is no longer used, but is kept here for backwards compatibility
+type FlexibleBoardPanelModelV0 struct {
+	PanelType  types.String `tfsdk:"type"`
+	Position   types.List   `tfsdk:"position"`
+	QueryPanel types.List   `tfsdk:"query_panel"`
+	SLOPanel   types.List   `tfsdk:"slo_panel"`
+}
diff --git a/internal/provider/flexible_board_resource.go b/internal/provider/flexible_board_resource.go
index 9dd81b8..8076f33 100644
--- a/internal/provider/flexible_board_resource.go
+++ b/internal/provider/flexible_board_resource.go
@@ -18,17 +18,20 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
 	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
 	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
 )
 
 // Ensure the implementation satisfies the expected interfaces.
 var (
-	_ resource.Resource                = &flexibleBoardResource{}
-	_ resource.ResourceWithConfigure   = &flexibleBoardResource{}
-	_ resource.ResourceWithImportState = &flexibleBoardResource{}
+	_ resource.Resource                 = &flexibleBoardResource{}
+	_ resource.ResourceWithConfigure    = &flexibleBoardResource{}
+	_ resource.ResourceWithImportState  = &flexibleBoardResource{}
+	_ resource.ResourceWithUpgradeState = &flexibleBoardResource{}
 )
 
 type flexibleBoardResource struct {
@@ -60,6 +63,7 @@ func (r *flexibleBoardResource) Configure(_ context.Context, req resource.Config
 func (*flexibleBoardResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
 	resp.Schema = schema.Schema{
 		Description: "Manages a flexible Board in a Honeycomb Environment.",
+		Version:     1,
 		Attributes: map[string]schema.Attribute{
 			"id": schema.StringAttribute{
 				Computed:    true,
@@ -100,6 +104,9 @@ func (*flexibleBoardResource) Schema(_ context.Context, _ resource.SchemaRequest
 		Blocks: map[string]schema.Block{
 			"panel": schema.ListNestedBlock{
 				Description: "List of panels to render on the board.",
+				Validators: []validator.List{
+					validation.RequireConsistentPanelPositions(),
+				},
 				NestedObject: schema.NestedBlockObject{
 					Attributes: map[string]schema.Attribute{
 						"type": schema.StringAttribute{
@@ -111,51 +118,47 @@ func (*flexibleBoardResource) Schema(_ context.Context, _ resource.SchemaRequest
 						},
 					},
 					Blocks: map[string]schema.Block{
-						"position": schema.ListNestedBlock{
+						"position": schema.SingleNestedBlock{
 							Description: `Manages the position of the panel on the board.`,
-							Validators: []validator.List{
-								listvalidator.SizeAtMost(1),
-								listvalidator.SizeAtLeast(1),
-							},
-							NestedObject: schema.NestedBlockObject{
-								Attributes: map[string]schema.Attribute{
-									"x_coordinate": schema.Int64Attribute{
-										Optional:    true,
-										Required:    false,
-										Computed:    true,
-										Description: "The X coordinate of the panel.",
-										Default:     int64default.StaticInt64(0),
-										Validators: []validator.Int64{
-											int64validator.AtLeast(0),
-										},
+							Attributes: map[string]schema.Attribute{
+								"x_coordinate": schema.Int64Attribute{
+									Optional:    true,
+									Computed:    true,
+									Required:    false,
+									Description: "The X coordinate of the panel.",
+									Validators: []validator.Int64{
+										int64validator.AtLeast(0),
+										int64validator.AlsoRequires(path.MatchRelative().AtParent().AtName("y_coordinate")),
 									},
-									"y_coordinate": schema.Int64Attribute{
-										Optional:    true,
-										Computed:    true,
-										Required:    false,
-										Description: "The Y coordinate of the panel.",
-										Default:     int64default.StaticInt64(0),
-										Validators: []validator.Int64{
-											int64validator.AtLeast(0),
-										},
+								},
+								"y_coordinate": schema.Int64Attribute{
+									Optional:    true,
+									Computed:    true,
+									Required:    false,
+									Description: "The Y coordinate of the panel.",
+									Validators: []validator.Int64{
+										int64validator.AtLeast(0),
+										int64validator.AlsoRequires(path.MatchRelative().AtParent().AtName("x_coordinate")),
 									},
-									"height": schema.Int64Attribute{
-										Optional:    true,
-										Computed:    true,
-										Required:    false,
-										Description: "The height of the panel.",
-										Validators: []validator.Int64{
-											int64validator.AtLeast(1),
-										},
+								},
+								"height": schema.Int64Attribute{
+									Optional:    true,
+									Computed:    true,
+									Required:    false,
+									Description: "The height of the panel.",
+									Validators: []validator.Int64{
+										int64validator.AtLeast(1),
+										int64validator.AtMost(12),
 									},
-									"width": schema.Int64Attribute{
-										Optional:    true,
-										Computed:    true,
-										Required:    false,
-										Description: "The width of the panel.",
-										Validators: []validator.Int64{
-											int64validator.AtLeast(1),
-										},
+								},
+								"width": schema.Int64Attribute{
+									Optional:    true,
+									Computed:    true,
+									Required:    false,
+									Description: "The width of the panel.",
+									Validators: []validator.Int64{
+										int64validator.AtLeast(1),
+										int64validator.AtMost(12),
 									},
 								},
 							},
@@ -289,6 +292,304 @@ func (r *flexibleBoardResource) ImportState(ctx context.Context, req resource.Im
 	resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
 }
 
+func (*flexibleBoardResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
+	// upgrade state as position is not an object and
+	return map[int64]resource.StateUpgrader{
+		0: {
+			PriorSchema: &schema.Schema{
+				Description: "Manages a flexible Board in a Honeycomb Environment.",
+				Attributes: map[string]schema.Attribute{
+					"id": schema.StringAttribute{
+						Computed:    true,
+						Required:    false,
+						Optional:    false,
+						Description: "The ID of the Board.",
+						PlanModifiers: []planmodifier.String{
+							stringplanmodifier.UseStateForUnknown(),
+						},
+					},
+					"name": schema.StringAttribute{
+						Required:    true,
+						Description: "The name of the Board.",
+						Validators: []validator.String{
+							stringvalidator.LengthBetween(1, 255),
+						},
+					},
+					"description": schema.StringAttribute{
+						Optional:    true,
+						Computed:    true,
+						Description: "The description of the Board. Supports Markdown.",
+						Default:     stringdefault.StaticString(""),
+						Validators: []validator.String{
+							stringvalidator.LengthBetween(1, 1023),
+						},
+					},
+					"board_url": schema.StringAttribute{
+						Computed:    true,
+						Required:    false,
+						Optional:    false,
+						Description: "The URL of the Board in the Honeycomb UI.",
+						PlanModifiers: []planmodifier.String{
+							stringplanmodifier.UseStateForUnknown(),
+						},
+					},
+					"tags": tagsSchema(),
+				},
+				Blocks: map[string]schema.Block{
+					"panel": schema.ListNestedBlock{
+						Description: "List of panels to render on the board.",
+						NestedObject: schema.NestedBlockObject{
+							Attributes: map[string]schema.Attribute{
+								"type": schema.StringAttribute{
+									Required:    true,
+									Description: `The panel type, either "query" or "slo".`,
+									Validators: []validator.String{
+										stringvalidator.OneOf("query", "slo"),
+									},
+								},
+							},
+							Blocks: map[string]schema.Block{
+								"position": schema.ListNestedBlock{
+									Description: `Manages the position of the panel on the board.`,
+									Validators: []validator.List{
+										listvalidator.SizeAtMost(1),
+										listvalidator.SizeAtLeast(1),
+									},
+									NestedObject: schema.NestedBlockObject{
+										Attributes: map[string]schema.Attribute{
+											"x_coordinate": schema.Int64Attribute{
+												Optional:    true,
+												Required:    false,
+												Computed:    true,
+												Description: "The X coordinate of the panel.",
+												Default:     int64default.StaticInt64(0),
+												Validators: []validator.Int64{
+													int64validator.AtLeast(0),
+												},
+											},
+											"y_coordinate": schema.Int64Attribute{
+												Optional:    true,
+												Computed:    true,
+												Required:    false,
+												Description: "The Y coordinate of the panel.",
+												Default:     int64default.StaticInt64(0),
+												Validators: []validator.Int64{
+													int64validator.AtLeast(0),
+												},
+											},
+											"height": schema.Int64Attribute{
+												Optional:    true,
+												Computed:    true,
+												Required:    false,
+												Description: "The height of the panel.",
+												Validators: []validator.Int64{
+													int64validator.AtLeast(1),
+												},
+											},
+											"width": schema.Int64Attribute{
+												Optional:    true,
+												Computed:    true,
+												Required:    false,
+												Description: "The width of the panel.",
+												Validators: []validator.Int64{
+													int64validator.AtLeast(1),
+												},
+											},
+										},
+									},
+								},
+								"slo_panel": schema.ListNestedBlock{
+									Description: "A Service Level Objective(SLO) panel to be displayed on the Board.",
+									Validators: []validator.List{
+										listvalidator.SizeAtMost(1),
+									},
+									NestedObject: schema.NestedBlockObject{
+										Attributes: map[string]schema.Attribute{
+											"slo_id": schema.StringAttribute{
+												Required:    true,
+												Description: "SLO ID to display in this panel.",
+											},
+										},
+									},
+								},
+								"query_panel": schema.ListNestedBlock{
+									Description: "A query panel to be displayed on the Board.",
+									Validators: []validator.List{
+										listvalidator.SizeAtMost(1),
+									},
+									NestedObject: schema.NestedBlockObject{
+										Attributes: map[string]schema.Attribute{
+											"query_id": schema.StringAttribute{
+												Required:    true,
+												Description: "Query ID to be rendered in the panel.",
+											},
+											"query_annotation_id": schema.StringAttribute{
+												Required:    true,
+												Optional:    false,
+												Description: "Query annotation ID.",
+											},
+											"query_style": schema.StringAttribute{
+												Optional:    true,
+												Computed:    true,
+												Description: "The visual style of the query (e.g., 'graph', 'combo').",
+												Validators: []validator.String{
+													stringvalidator.OneOf("graph", "table", "combo"),
+												},
+												Default: stringdefault.StaticString("graph"),
+											},
+										},
+										Blocks: map[string]schema.Block{
+											"visualization_settings": schema.ListNestedBlock{
+												Validators: []validator.List{
+													listvalidator.SizeAtMost(1),
+												},
+												NestedObject: schema.NestedBlockObject{
+													Attributes: map[string]schema.Attribute{
+														"use_utc_xaxis": schema.BoolAttribute{
+															Optional:    true,
+															Computed:    true,
+															Description: "Display UTC Time X-Axis or Localtime X-Axis.",
+														},
+														"hide_markers": schema.BoolAttribute{
+															Optional:    true,
+															Computed:    true,
+															Description: "Hide markers from appearing on graph.",
+														},
+														"hide_hovers": schema.BoolAttribute{
+															Optional:    true,
+															Computed:    true,
+															Description: "Disable Graph tooltips in the results display when hovering over a graph.",
+														},
+														"prefer_overlaid_charts": schema.BoolAttribute{
+															Optional:    true,
+															Computed:    true,
+															Description: "Combine any visualized AVG, MIN, MAX, and PERCENTILE clauses into a single chart.",
+														},
+														"hide_compare": schema.BoolAttribute{
+															Optional:    true,
+															Computed:    true,
+															Description: "Hide comparison values.",
+														},
+													},
+													Blocks: map[string]schema.Block{
+														"chart": schema.ListNestedBlock{
+															NestedObject: schema.NestedBlockObject{
+																Attributes: map[string]schema.Attribute{
+																	"chart_type": schema.StringAttribute{
+																		Optional:    true,
+																		Computed:    true,
+																		Description: "Type of chart (e.g., 'line', 'bar').",
+																		Validators: []validator.String{
+																			stringvalidator.OneOf("default", "line", "tsbar", "stacked", "stat", "tsbar", "cpie", "cbar"),
+																		},
+																		Default: stringdefault.StaticString("default"),
+																	},
+																	"chart_index": schema.Int64Attribute{
+																		Optional:    true,
+																		Computed:    true,
+																		Description: "Index of the chart this configuration controls.",
+																		Validators: []validator.Int64{
+																			int64validator.AtLeast(0),
+																		},
+																	},
+																	"omit_missing_values": schema.BoolAttribute{
+																		Optional:    true,
+																		Computed:    true,
+																		Description: "Omit missing values from the visualization.",
+																	},
+																	"use_log_scale": schema.BoolAttribute{
+																		Optional:    true,
+																		Computed:    true,
+																		Description: "Use logarithmic scale on Y axis.",
+																	},
+																},
+															},
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
+				var oldState models.FlexibleBoardResourceModel
+				resp.Diagnostics.Append(req.State.Get(ctx, &oldState)...)
+				if resp.Diagnostics.HasError() {
+					return
+				}
+
+				if oldState.Panels.IsNull() || oldState.Panels.IsUnknown() {
+					return
+				}
+
+				// convert the old state to the new state
+				var statePanels []models.FlexibleBoardPanelModelV0
+				resp.Diagnostics.Append(oldState.Panels.ElementsAs(ctx, &statePanels, false)...)
+				if resp.Diagnostics.HasError() {
+					return
+				}
+
+				var newStatePanels []models.BoardPanelModel
+				for _, panel := range statePanels {
+					upgradedPanel := models.BoardPanelModel{
+						PanelType:  panel.PanelType,
+						QueryPanel: panel.QueryPanel,
+						SLOPanel:   panel.SLOPanel,
+					}
+
+					if !(panel.Position.IsNull() || panel.Position.IsUnknown()) {
+						var oldStylePositions []models.BoardPanelPositionModel
+						resp.Diagnostics.Append(panel.Position.ElementsAs(ctx, &oldStylePositions, false)...)
+						if resp.Diagnostics.HasError() {
+							return
+						}
+
+						if len(oldStylePositions) == 0 {
+							upgradedPanel.Position = types.ObjectNull(models.BoardPanelPositionModelAttrType)
+						} else {
+							attrs := map[string]attr.Value{}
+							attrs["x_coordinate"] = oldStylePositions[0].XCoordinate
+							attrs["y_coordinate"] = oldStylePositions[0].YCoordinate
+							attrs["height"] = oldStylePositions[0].Height
+							attrs["width"] = oldStylePositions[0].Width
+
+							obj, d := types.ObjectValue(models.BoardPanelPositionModelAttrType, attrs)
+							resp.Diagnostics.Append(d...)
+
+							upgradedPanel.Position = obj
+						}
+					} else {
+						upgradedPanel.Position = types.ObjectNull(models.BoardPanelPositionModelAttrType)
+					}
+
+					newStatePanels = append(newStatePanels, upgradedPanel)
+				}
+
+				finalPanels, diags := types.ListValueFrom(ctx,
+					types.ObjectType{AttrTypes: models.BoardPanelModelAttrType},
+					newStatePanels,
+				)
+				resp.Diagnostics.Append(diags...)
+
+				newState := models.FlexibleBoardResourceModel{
+					ID:          oldState.ID,
+					Name:        oldState.Name,
+					Description: oldState.Description,
+					URL:         oldState.URL,
+					Panels:      finalPanels,
+					Tags:        oldState.Tags,
+				}
+				resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
+			},
+		},
+	}
+}
+
 func (r *flexibleBoardResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
 	var plan, config models.FlexibleBoardResourceModel
 	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
@@ -303,12 +604,14 @@ func (r *flexibleBoardResource) Create(ctx context.Context, req resource.CreateR
 	}
 
 	panelFromConfig := expandBoardPanels(ctx, plan.Panels, &resp.Diagnostics)
+	finalPanels, layoutGeneration := setAPIDefaultsAndDetermineLayoutGeneration(panelFromConfig)
 	createRequest := &client.Board{
-		Name:        plan.Name.ValueString(),
-		Description: plan.Description.ValueString(),
-		BoardType:   client.BoardTypeFlexible,
-		Panels:      removeDefaultNegativeNumbers(panelFromConfig),
-		Tags:        planTags,
+		Name:             plan.Name.ValueString(),
+		Description:      plan.Description.ValueString(),
+		BoardType:        client.BoardTypeFlexible,
+		Panels:           finalPanels,
+		Tags:             planTags,
+		LayoutGeneration: layoutGeneration,
 	}
 
 	if resp.Diagnostics.HasError() {
@@ -325,6 +628,7 @@ func (r *flexibleBoardResource) Create(ctx context.Context, req resource.CreateR
 	state.Name = types.StringValue(board.Name)
 	state.Description = types.StringValue(board.Description)
 	state.URL = types.StringValue(board.Links.BoardURL)
+	// state.LayoutGeneration = plan.LayoutGeneration
 
 	if len(board.Panels) == 0 {
 		state.Panels = types.ListNull(types.ObjectType{AttrTypes: models.BoardPanelModelAttrType})
@@ -457,13 +761,15 @@ func (r *flexibleBoardResource) Update(ctx context.Context, req resource.UpdateR
 	}
 
 	panelConfig := expandBoardPanels(ctx, plan.Panels, &resp.Diagnostics)
+	finalPanels, layoutGeneration := setAPIDefaultsAndDetermineLayoutGeneration(panelConfig)
 	updateRequest := &client.Board{
-		ID:          plan.ID.ValueString(),
-		Name:        plan.Name.ValueString(),
-		Description: plan.Description.ValueString(),
-		BoardType:   client.BoardTypeFlexible,
-		Panels:      removeDefaultNegativeNumbers(panelConfig),
-		Tags:        planTags,
+		ID:               plan.ID.ValueString(),
+		Name:             plan.Name.ValueString(),
+		Description:      plan.Description.ValueString(),
+		BoardType:        client.BoardTypeFlexible,
+		Panels:           finalPanels,
+		Tags:             planTags,
+		LayoutGeneration: layoutGeneration,
 	}
 	if resp.Diagnostics.HasError() {
 		return
@@ -581,31 +887,26 @@ func expandBoardPanels(
 // This is a workaround for the limitations of the terraform v5 protocol.
 func expandPanelPosition(
 	ctx context.Context,
-	panelPosition types.List,
+	panelPosition types.Object,
 	diags *diag.Diagnostics,
 ) client.BoardPanelPosition {
 	if panelPosition.IsNull() || panelPosition.IsUnknown() {
 		return client.BoardPanelPosition{
-			X: -1,
-			Y: -1,
+			X:      -1,
+			Y:      -1,
+			Width:  -1,
+			Height: -1,
 		}
 	}
 
-	var position []models.BoardPanelPositionModel
-	diags.Append(panelPosition.ElementsAs(ctx, &position, false)...)
-
-	if len(position) == 0 {
-		return client.BoardPanelPosition{
-			X: -1,
-			Y: -1,
-		}
-	}
+	var position models.BoardPanelPositionModel
+	diags.Append(panelPosition.As(ctx, &position, basetypes.ObjectAsOptions{})...)
 
 	return client.BoardPanelPosition{
-		X:      int(position[0].XCoordinate.ValueInt64()),
-		Y:      int(position[0].YCoordinate.ValueInt64()),
-		Height: int(position[0].Height.ValueInt64()),
-		Width:  int(position[0].Width.ValueInt64()),
+		X:      int(position.XCoordinate.ValueInt64()),
+		Y:      int(position.YCoordinate.ValueInt64()),
+		Height: int(position.Height.ValueInt64()),
+		Width:  int(position.Width.ValueInt64()),
 	}
 }
 
@@ -716,40 +1017,44 @@ func flattenBoardPanel(
 	panelValue := make(map[string]attr.Value)
 	panelValue["type"] = types.StringValue(string(panel.PanelType))
 	panelValue["slo_panel"] = flattenBoardSloPanel(ctx, panel.SLOPanel, diags)
-	panelValue["position"] = flattenBoardPanelPosition(ctx, panel.PanelPosition, diags, statePanel.PanelPosition)
+	panelValue["position"] = flattenBoardPanelPosition(panel.PanelPosition, diags, statePanel.PanelPosition)
 	panelValue["query_panel"] = flattenBoardQueryPanel(ctx, panel.QueryPanel, diags)
 
 	return panelValue
 }
 
 func flattenBoardPanelPosition(
-	ctx context.Context,
 	position client.BoardPanelPosition,
 	diags *diag.Diagnostics,
 	statePosition client.BoardPanelPosition,
-) types.List {
+) types.Object {
 	// we use negative numbers to indicate that the panel position was never set. We use this to not write to state when panel position is not set.
 	// This is a workaround for the various limitations that terraform v5 protocol presents.
 	// Without this workaround, whenever the API generates a default position, terraform would complain about a schema mismatch between config and applied results.
-	if statePosition.Height == 0 && statePosition.Width == 0 && statePosition.X == -1 && statePosition.Y == -1 {
-		return types.ListNull(types.ObjectType{AttrTypes: models.BoardPanelPositionModelAttrType})
-	}
-	obj, d := types.ObjectValue(models.BoardPanelPositionModelAttrType, map[string]attr.Value{
-		"x_coordinate": types.Int64Value(int64(position.X)),
-		"y_coordinate": types.Int64Value(int64(position.Y)),
-		"height":       types.Int64Value(int64(position.Height)),
-		"width":        types.Int64Value(int64(position.Width)),
-	})
-	diags.Append(d...)
+	if statePosition.Height == -1 && statePosition.Width == -1 && statePosition.X == -1 && statePosition.Y == -1 {
+		return types.ObjectNull(models.BoardPanelPositionModelAttrType)
+	}
+	x := position.X
+	y := position.Y
+	width := position.Width
+	height := position.Height
+
+	attrs := map[string]attr.Value{}
+	if statePosition.X != -1 && statePosition.Y != -1 {
+		attrs["x_coordinate"] = types.Int64Value(int64(x))
+		attrs["y_coordinate"] = types.Int64Value(int64(y))
+	}
+	if statePosition.Width != -1 {
+		attrs["width"] = types.Int64Value(int64(width))
+	}
+	if statePosition.Height != -1 {
+		attrs["height"] = types.Int64Value(int64(height))
+	}
 
-	result, d := types.ListValueFrom(
-		ctx,
-		types.ObjectType{AttrTypes: models.BoardPanelPositionModelAttrType},
-		[]attr.Value{obj},
-	)
+	obj, d := types.ObjectValue(models.BoardPanelPositionModelAttrType, attrs)
 	diags.Append(d...)
 
-	return result
+	return obj
 }
 
 func flattenBoardQueryPanel(
@@ -868,23 +1173,40 @@ func flattenBoardSloPanel(
 	return result
 }
 
-// we use negative numbers to indicate that the panel position was never set.
+// setAPIDefaultsAndDetermineLayoutGeneration removes negative numbers. we use negative numbers to indicate that the panel position was never set.
 // This is a workaround for the various limitations that terraform v5 protocol presents.
 // The API will set default panel positions based on panel type. We decided not to write
 // position to state when the panel position is not set.
-func removeDefaultNegativeNumbers(panels []client.BoardPanel) []client.BoardPanel {
+// It returns layout generation "auto" if all panels have no positions.
+func setAPIDefaultsAndDetermineLayoutGeneration(panels []client.BoardPanel) ([]client.BoardPanel, client.LayoutGeneration) {
 	if len(panels) == 0 {
-		return panels
+		return panels, client.LayoutGenerationAuto
 	}
 	resp := make([]client.BoardPanel, len(panels))
 	copy(resp, panels)
 
+	blankCount := 0
+
 	for i := range resp {
 		if resp[i].PanelPosition.X == -1 && resp[i].PanelPosition.Y == -1 {
 			resp[i].PanelPosition.X = 0
 			resp[i].PanelPosition.Y = 0
 		}
+		if resp[i].PanelPosition.Width == -1 {
+			resp[i].PanelPosition.Width = 0
+		}
+		if resp[i].PanelPosition.Height == -1 {
+			resp[i].PanelPosition.Height = 0
+		}
+
+		if resp[i].IsBlank() {
+			blankCount++
+		}
+	}
+
+	if blankCount == len(resp) {
+		return resp, client.LayoutGenerationAuto
 	}
 
-	return resp
+	return resp, client.LayoutGenerationManual
 }


Test patch:
diff --git a/client/board_test.go b/client/board_test.go
index 3c5ca4c..cd459a3 100644
--- a/client/board_test.go
+++ b/client/board_test.go
@@ -248,6 +248,55 @@ func TestFlexibleBoards(t *testing.T) {
 		assert.Equal(t, data, flexibleBoard)
 	})
 
+	t.Run("Create flexible board with auto layout generation", func(t *testing.T) {
+		data := &client.Board{
+			Name:             test.RandomStringWithPrefix("test.", 8),
+			BoardType:        "flexible",
+			Description:      "A board with some panels",
+			LayoutGeneration: client.LayoutGenerationAuto,
+			Panels: []client.BoardPanel{
+				{
+					PanelType: client.BoardPanelTypeQuery,
+					QueryPanel: &client.BoardQueryPanel{
+						QueryID:           *query.ID,
+						QueryAnnotationID: queryAnnotation.ID,
+						Style:             client.BoardQueryStyleGraph,
+					},
+				},
+				{
+					PanelType: client.BoardPanelTypeSLO,
+					SLOPanel: &client.BoardSLOPanel{
+						SLOID: slo.ID,
+					},
+				},
+			},
+		}
+		flexibleBoard, err = c.Boards.Create(ctx, data)
+		require.NoError(t, err)
+		assert.NotNil(t, flexibleBoard.ID)
+
+		// copy ID before asserting equality
+		data.ID = flexibleBoard.ID
+
+		// ensure the board URL got populated
+		assert.NotEmpty(t, flexibleBoard.Links.BoardURL)
+		data.Links.BoardURL = flexibleBoard.Links.BoardURL
+
+		for i, panel := range flexibleBoard.Panels {
+			assert.Equal(t, data.Panels[i].PanelType, panel.PanelType)
+			assert.Equal(t, data.Panels[i].QueryPanel, panel.QueryPanel)
+			assert.Equal(t, data.Panels[i].SLOPanel, panel.SLOPanel)
+
+			// since positions are auto generated, we can't assert their exact values
+			// but we can assert that they are not empty
+			assert.NotEmpty(t, panel.PanelPosition)
+			assert.GreaterOrEqual(t, panel.PanelPosition.X, 0)
+			assert.GreaterOrEqual(t, panel.PanelPosition.Y, 0)
+			assert.Positive(t, panel.PanelPosition.Height)
+			assert.Positive(t, panel.PanelPosition.Width)
+		}
+	})
+
 	t.Run("Create flexible board with tags", func(t *testing.T) {
 		data := &client.Board{
 			Name:        test.RandomStringWithPrefix("test.", 8),
diff --git a/internal/helper/validation/panel_positions_consistency_test.go b/internal/helper/validation/panel_positions_consistency_test.go
new file mode 100644
index 0000000..4a4bb08
--- /dev/null
+++ b/internal/helper/validation/panel_positions_consistency_test.go
@@ -0,0 +1,259 @@
+package validation_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation"
+)
+
+func Test_RequireConsistentPanelPositionsValidator(t *testing.T) {
+	t.Parallel()
+
+	positionAttrTypes := map[string]attr.Type{
+		"x_coordinate": types.Int64Type,
+		"y_coordinate": types.Int64Type,
+		"height":       types.Int64Type,
+		"width":        types.Int64Type,
+	}
+
+	panelAttrTypes := map[string]attr.Type{
+		"type": types.StringType,
+		"position": types.ObjectType{
+			AttrTypes: positionAttrTypes,
+		},
+		"query_panel": types.ListType{
+			ElemType: types.ObjectType{
+				AttrTypes: map[string]attr.Type{
+					"query_id":            types.StringType,
+					"query_annotation_id": types.StringType,
+				},
+			},
+		},
+		"slo_panel": types.ListType{
+			ElemType: types.ObjectType{
+				AttrTypes: map[string]attr.Type{
+					"slo_id": types.StringType,
+				},
+			},
+		},
+	}
+
+	panelListType := types.ListType{
+		ElemType: types.ObjectType{
+			AttrTypes: panelAttrTypes,
+		},
+	}
+
+	tests := []struct {
+		name        string
+		value       types.List
+		expectError bool
+	}{
+		{
+			name:  "unknown",
+			value: types.ListUnknown(panelListType.ElemType),
+		},
+		{
+			name:  "null",
+			value: types.ListNull(panelListType.ElemType),
+		},
+		{
+			name:  "empty list",
+			value: types.ListValueMust(panelListType.ElemType, []attr.Value{}),
+		},
+		{
+			name: "single panel with position",
+			value: types.ListValueMust(panelListType.ElemType, []attr.Value{
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type": types.StringValue("query"),
+					"position": types.ObjectValueMust(positionAttrTypes, map[string]attr.Value{
+						"x_coordinate": types.Int64Value(10),
+						"y_coordinate": types.Int64Value(20),
+						"height":       types.Int64Null(),
+						"width":        types.Int64Null(),
+					}),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+			}),
+		},
+		{
+			name: "single panel without position",
+			value: types.ListValueMust(panelListType.ElemType, []attr.Value{
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type":     types.StringValue("query"),
+					"position": types.ObjectNull(positionAttrTypes),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+			}),
+		},
+		{
+			name: "all panels have positions",
+			value: types.ListValueMust(panelListType.ElemType, []attr.Value{
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type": types.StringValue("query"),
+					"position": types.ObjectValueMust(positionAttrTypes, map[string]attr.Value{
+						"x_coordinate": types.Int64Value(10),
+						"y_coordinate": types.Int64Value(20),
+						"height":       types.Int64Null(),
+						"width":        types.Int64Null(),
+					}),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type": types.StringValue("slo"),
+					"position": types.ObjectValueMust(positionAttrTypes, map[string]attr.Value{
+						"x_coordinate": types.Int64Value(30),
+						"y_coordinate": types.Int64Value(40),
+						"height":       types.Int64Null(),
+						"width":        types.Int64Null(),
+					}),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+			}),
+		},
+		{
+			name: "no panels have positions",
+			value: types.ListValueMust(panelListType.ElemType, []attr.Value{
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type":     types.StringValue("query"),
+					"position": types.ObjectNull(positionAttrTypes),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type":     types.StringValue("slo"),
+					"position": types.ObjectNull(positionAttrTypes),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+			}),
+		},
+		{
+			name: "mixed panels - some with positions, some without",
+			value: types.ListValueMust(panelListType.ElemType, []attr.Value{
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type": types.StringValue("query"),
+					"position": types.ObjectValueMust(positionAttrTypes, map[string]attr.Value{
+						"x_coordinate": types.Int64Value(10),
+						"y_coordinate": types.Int64Value(20),
+						"height":       types.Int64Null(),
+						"width":        types.Int64Null(),
+					}),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+				types.ObjectValueMust(panelAttrTypes, map[string]attr.Value{
+					"type":     types.StringValue("slo"),
+					"position": types.ObjectNull(positionAttrTypes),
+					"query_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"query_id":            types.StringType,
+							"query_annotation_id": types.StringType,
+						},
+					}),
+					"slo_panel": types.ListNull(types.ObjectType{
+						AttrTypes: map[string]attr.Type{
+							"slo_id": types.StringType,
+						},
+					}),
+				}),
+			}),
+			expectError: true,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			request := validator.ListRequest{
+				Path:           path.Root("test"),
+				PathExpression: path.MatchRoot("test"),
+				ConfigValue:    tc.value,
+			}
+			response := validator.ListResponse{}
+
+			validation.RequireConsistentPanelPositions().ValidateList(context.Background(), request, &response)
+
+			assert.Equal(t,
+				tc.expectError,
+				response.Diagnostics.HasError(),
+				"unexpected error: %s", response.Diagnostics,
+			)
+		})
+	}
+}
diff --git a/internal/provider/flexible_board_resource_test.go b/internal/provider/flexible_board_resource_test.go
index 10c66f0..5829b4c 100644
--- a/internal/provider/flexible_board_resource_test.go
+++ b/internal/provider/flexible_board_resource_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+	"github.com/hashicorp/terraform-plugin-testing/plancheck"
 	"github.com/stretchr/testify/require"
 )
 
@@ -47,11 +48,16 @@ func TestAccHoneycombioFlexibleBoard(t *testing.T) {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "name", "Test flexible board from terraform-provider-honeycombio"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "description", "Test flexible board description"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.#", "2"),
+
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.type", "slo"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.query_panel.#", "0"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.slo_panel.#", "1"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.slo_panel.0.slo_id", slo.ID),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.#", "0"), // confirms that position is not set
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.height", "4"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.width", "3"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.x_coordinate", "0"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.y_coordinate", "0"),
+
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.type", "query"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.slo_panel.#", "0"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.#", "1"),
@@ -66,6 +72,10 @@ func TestAccHoneycombioFlexibleBoard(t *testing.T) {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.0.visualization_settings.0.chart.0.chart_index", "0"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.0.visualization_settings.0.chart.0.omit_missing_values", "true"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.0.visualization_settings.0.chart.0.use_log_scale", "false"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.height", "6"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.width", "6"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.x_coordinate", "0"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.y_coordinate", "3"),
 				),
 			},
 			// remove board's panels, add tags
@@ -90,7 +100,7 @@ resource "honeycombio_flexible_board" "test" {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.env", "dev"),
 				),
 			},
-			// now add a query panel, update tags, and ensure the board is updated
+			// now add a query panel with no position (auto generated positions), update tags, and ensure the board is updated
 			{
 				Config: fmt.Sprintf(`
 data "honeycombio_query_specification" "test" {
@@ -116,7 +126,7 @@ resource "honeycombio_flexible_board" "test" {
   description = "simple flexible board description"
   tags = {
 	team = "green"
-  } 
+  }
   panel {
     type = "query"
     query_panel {
@@ -145,9 +155,14 @@ resource "honeycombio_flexible_board" "test" {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.%", "1"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.team", "green"),
 					resource.TestCheckNoResourceAttr("honeycombio_flexible_board.test", "tags.env"),
+					// when position not provided, the position should not be set in state
+					resource.TestCheckNoResourceAttr("honeycombio_flexible_board.test", "panel.0.position.height"),
+					resource.TestCheckNoResourceAttr("honeycombio_flexible_board.test", "panel.0.position.width"),
+					resource.TestCheckNoResourceAttr("honeycombio_flexible_board.test", "panel.0.position.x_coordinate"),
+					resource.TestCheckNoResourceAttr("honeycombio_flexible_board.test", "panel.0.position.y_coordinate"),
 				),
 			},
-			// now add an SLO panel, remove chart settings from the query panel, remove tags
+			// now add an SLO panel, remove chart settings from the query panel, remove tags and update from generated positions to provided positions
 			{
 				Config: fmt.Sprintf(`
 data "honeycombio_query_specification" "test" {
@@ -173,10 +188,6 @@ resource "honeycombio_flexible_board" "test" {
   description = "simple flexible board description"
   panel {
     type = "query"
-    position {
-      x_coordinate = 0
-      y_coordinate = 0
-    }
     query_panel {
       query_id            = honeycombio_query.test.id
       query_annotation_id = honeycombio_query_annotation.test.id
@@ -184,16 +195,25 @@ resource "honeycombio_flexible_board" "test" {
       visualization_settings {
         use_utc_xaxis = false
       }
+    }
+	position {
+      height       = 4
+      width        = 3
+      x_coordinate = 0
+      y_coordinate = 0
     }
   }
 
   panel {
     type = "slo"
-    position {
-      x_coordinate = 0
-    }
     slo_panel {
       slo_id = "%[2]s"
+    }
+	position {
+      height       = 4
+      width        = 3
+      x_coordinate = 3
+      y_coordinate = 0
     }
   }
 }
@@ -210,15 +230,15 @@ resource "honeycombio_flexible_board" "test" {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.query_panel.0.visualization_settings.#", "1"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.query_panel.0.visualization_settings.0.use_utc_xaxis", "false"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.query_panel.0.visualization_settings.0.chart.#", "0"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.#", "1"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.type", "slo"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.slo_panel.#", "1"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.slo_panel.0.slo_id", slo.ID),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.#", "1"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.x_coordinate", "0"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.y_coordinate", "0"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.height", "4"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.width", "3"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.height", "4"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.width", "3"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.x_coordinate", "0"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.y_coordinate", "0"),
+
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.height", "4"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.width", "3"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.x_coordinate", "3"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.y_coordinate", "0"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.%", "0"),
 				),
 			},
@@ -250,10 +270,8 @@ resource "honeycombio_flexible_board" "test" {
   panel {
     type = "slo"
     position {
-      x_coordinate = 0
-      y_coordinate = 0
-      height       = 4
-      width        = 3
+      height = 4
+      width  = 3
     }
     slo_panel {
       slo_id = "%[2]s"
@@ -269,6 +287,7 @@ resource "honeycombio_flexible_board" "test" {
     }
     position {
       height = 5
+      width  = 6
     }
   }
 }
@@ -281,24 +300,77 @@ resource "honeycombio_flexible_board" "test" {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.type", "slo"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.slo_panel.#", "1"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.slo_panel.0.slo_id", slo.ID),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.#", "1"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.0.x_coordinate", "0"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.0.y_coordinate", "0"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.0.height", "4"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.0.width", "3"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.height", "4"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.0.position.width", "3"),
 					resource.TestCheckResourceAttrPair("honeycombio_flexible_board.test", "panel.1.query_panel.0.query_id", "honeycombio_query.test", "id"),
 					resource.TestCheckResourceAttrPair("honeycombio_flexible_board.test", "panel.1.query_panel.0.query_annotation_id", "honeycombio_query_annotation.test", "id"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.#", "1"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.0.query_style", "table"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.0.visualization_settings.#", "0"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.type", "query"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.#", "1"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.x_coordinate", "0"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.y_coordinate", "0"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.height", "5"),
-					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.width", "6"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.height", "5"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.width", "6"),
+					// ensure x and y coordinates are dynamically generated when only height and width are provided
+					resource.TestCheckResourceAttrSet("honeycombio_flexible_board.test", "panel.1.position.x_coordinate"),
+					resource.TestCheckResourceAttrSet("honeycombio_flexible_board.test", "panel.1.position.y_coordinate"),
+				),
+			},
+		},
+	})
+}
+
+// TestAccHoneycombioFlexibleBoard_upgradeFromVersion036_2 tests that the flexible board resource can be upgraded from version 0.36.2
+// This is needed because the position field was changed from a list to a single object causing a schema drift.
+// This test ensures the conversion works as expected.
+func TestAccHoneycombioFlexibleBoard_upgradeFromVersion036_2(t *testing.T) {
+	ctx := context.Background()
+	dataset := testAccDataset()
+	c := testAccClient(t)
+
+	sli, err := c.DerivedColumns.Create(ctx, dataset, &client.DerivedColumn{
+		Alias:      "sli." + acctest.RandString(8),
+		Expression: "BOOL(1)",
+	})
+	require.NoError(t, err)
+	slo, err := c.SLOs.Create(ctx, dataset, &client.SLO{
+		Name:             acctest.RandString(8) + " SLO",
+		TimePeriodDays:   14,
+		TargetPerMillion: 995000,
+		SLI:              client.SLIRef{Alias: sli.Alias},
+	})
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		// remove SLOs, and SLIs at end of test run
+		c.SLOs.Delete(ctx, dataset, slo.ID)
+		c.DerivedColumns.Delete(ctx, dataset, sli.ID)
+	})
+
+	config := testFlexibleBoardConfig(dataset, slo.ID)
+
+	resource.Test(t, resource.TestCase{
+		Steps: []resource.TestStep{
+			{
+				ExternalProviders: map[string]resource.ExternalProvider{
+					"honeycombio": {
+						VersionConstraint: "0.36.2",
+						Source:            "honeycombio/honeycombio",
+					},
+				},
+				Config: config,
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckBoardExists(t, "honeycombio_flexible_board.test"),
 				),
 			},
+			{
+				ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+				Config:                   config,
+				ConfigPlanChecks: resource.ConfigPlanChecks{
+					PreApply: []plancheck.PlanCheck{
+						plancheck.ExpectEmptyPlan(),
+					},
+				},
+			},
 		},
 	})
 }
@@ -330,6 +402,12 @@ resource "honeycombio_flexible_board" "test" {
     slo_panel {
       slo_id = "%[2]s"
     }
+    position {
+      height       = 4
+      width        = 3
+      x_coordinate = 0
+      y_coordinate = 0
+    }
   }
   panel {
     type = "query"
@@ -346,10 +424,10 @@ resource "honeycombio_flexible_board" "test" {
       }
     }
     position {
-      x_coordinate = 0
-      y_coordinate = 0
       height       = 6
       width        = 6
+      x_coordinate = 0
+      y_coordinate = 3
     }
   }
 }