Data Studio — The Authoring Surface
The Data Studio is one window for everything: lookup, search, cell editing, row/column/tab authoring, sheet creation and deletion, and a record canvas that shows what a record is made of.
One rule underlies all of it: the sheet is canonical. The Studio writes to the source (local file / Google sheet), never to the SOs, and it confirms every change with an automatic re-import.
Tools ▸ SheetForge ▸ Data Studio opens it — the one authoring entry in the menu. It needs no plugins: everything below works with the core built-in types alone, and a plugin adds shapes and read-only key spaces on top (see With and without plugins).
There is only one authoring window. The older Workbench — the original IMGUI window — has been removed, and everything it could do, the Studio does. See what happened to the Workbench if you are upgrading.
What is on screen
Topbar
- The active source chip names the provider. It is dimmed when no settings are active.
- ↓ Pull from source runs Import.
- ↑ Push to source carries a pill counting pending changes.
- A Discard button sits beside it and throws away every change staged in the window. It asks for a confirmation naming the count first, and it lands as a single
Ctrl+Zstep, so the staging comes back if you discard by mistake. The source is never touched. - The
isolated/draftbadges. - ⛔/⚠ counters. Clicking a counter jumps to the first error/warning.
- A ⋯ overflow menu holding Run Export, Push to Google Sheet, Export translations…, Import translations…, Health Check, Open the localization report and Theme Settings…. Outside a Google API source the Google push line stays visible but disabled, with the reason attached to the label; the two translation lines behave the same way when the project holds no localization sheet.
Sidebar
- An All entry sits at the top — cross-sheet search, described below.
- Then every sheet with its row count, its colour dot, and a ● marker on any sheet holding staged changes. The topbar pill gives the total; this says which sheets it came from.
- Sheets are grouped into sections by the sheet's own
@stylegroup label. Right-click a sheet ▸ Group & color ✎ to set them; sheets with no label stay in the default section. - Below the sheets is a READ-ONLY group listing plugin-registered code registries.
- Honest badges live here too:
parse failedon a tab that is missing from the preview,name clashwhen a code registry claims a real sheet's name, and the quarantine notice described below. - Each right-click item that is currently blocked stays visible with its reason attached.
Table
- A sticky four-row header: the source sheet column letter over the raw
@name/@type/@desccells. Column A holds the markers, so data starts at B. - A gutter with the real sheet row number, and a per-row reference chip:
shared Nwhen two or more records point at this row,↩ 1when exactly one does, nothing when none. - Letter and row number are the same coordinates the error report prints, so "row 5, column C" can be pointed at directly. Clicking them selects the column or row for multi-target actions and drag moves — see Selecting rows and columns below.
- The letter belongs to the seat, not to the column. Move a column and it shows the letter of the position it will sit in once reflected. The same rule gives a column you have added but not pushed the letter it is about to get.
- Structure editing lives in right-click menus. A column header offers properties, move, insert either side and delete; the row-number gutter offers move up, move down and delete. A blocked item stays visible with its reason.
- A column header also offers Freeze up to this column, which holds that column and everything left of it outside the horizontal scroll (see below).
- The rest of the column menu — column width, fit to data, hide, filter, clear column data and the sort items — is described under Column width, hiding and per-column filters below. A drag handle for resizing sits at the right edge of each letter cell.
- Inside a reference cell, only the broken list fragments are highlighted.
Canvas
- Opens below the table when you activate a row.
- The record you opened sits in the middle: what it consumes flows in from the left, what consumes it stands to the right. A segmented control above the canvas picks how far each way.
- Drag the grip to resize,
Esccloses it. - Each node card shows its reference ports and its plain columns as editable value rows, so the canvas is a second place to edit, not a picture (see Editing on the canvas below).
Node inspector
Bottom-right of the canvas. It holds:
- the node's cell address (click to focus that cell in the grid);
- a "shared record" / "this key does not exist" / "this record is new" note;
- the Used by list (click an entry to walk to it);
- the built-in Go to this sheet action;
- Duplicate and split / Cancel new row where they apply.
Problems
The bottom panel carries the same diagnostics an import produces, rendered by the same report renderer. The pipeline is the source of truth; the window never invents a message. Clicking a row focuses the offending cell and opens its canvas.
Domain panels
A right-hand pane with one segment per plugin-registered panel, collapsible, remembering which one you had open. It is drawn only when a plugin registers a panel — with none registered there is no empty box, because an empty box claims something is missing.
Push modal
The pre-write diff: tab · row key · field · old → new for every staged change, plus a note when some edits are isolated. An Addressables section lists every asset registration the reflect will make (add · move · create group — see Editing a cell). Cancel writes nothing.
Editing a cell
- The widget follows the
@type. The@typeit reads is the one on screen, so a column you added or retyped but have not pushed already gets that type's widget.- A required
boolis a toggle; an optionalboolopens the same picker the enum cell uses, since it has three states. Enum<T>reads as its value plus a ▾ and opens the same searchable picker window a reference cell uses. An unrecognised value stays at the top of the list rather than being silently rewritten. Where the enum is defined in a sheet, the picker ends with "Add a new member…". An empty optional cell shows —, or — (default).- A
RecordId@Tabcolumn gets a reference dropdown (below). - An
AssetRef@Groupcolumn gets an asset picker and takes drops (below). Color/AnimationCurve/Gradientcolumns get Unity's own colour, curve and gradient fields (below).- Everything else is canonical raw text — numbers, strings, keys,
List<>of non-references, wrappers, custom types — because that text is what the parser round-trips.
- A required
- Reference cells have a dropdown, always. A
RecordId@Tabcell is a text box plus a ▾ button; typing a key by hand still works. The button is there even when the target sheet currently offers no candidates: that state usually means something is broken right now, which is exactly when you need the tool, and the picker says so and offers to create instead of opening an empty list.- A
List<RecordId@Tab>cell gets+▾. The picker stays open and each row is a checkbox, so ticking three records takes one visit. Ticking appends, unticking removes that element, and each tick is its own undo step. - Element order is not what a dropdown answers. Reorder elements from the canvas, where each list element has its own ▲▼ row.
- A scalar cell replaces its value and closes, with the key already in the cell marked so you cannot pick it twice.
- A
- An optional column can be set back to empty. Where the column declares
?or=default, the pickers — reference, enum and optional-boolean alike — carry a leave-empty row labelled with the value the cell falls back to. A required column gets no such row, because an empty cell is an error there. - Create the record you are missing, from the cell. If the key you want does not exist, press + Create a new … record in the picker: the search box turns into a key box pre-filled with the sheet's common prefix. Type a name, press Enter, and the row is created and written into this cell as one
Ctrl+Zstep.- The line is not offered when it could not work. A code-owned key space cannot be added to, and the picker says so in a sentence. A read-only source, a running pipeline or an xlsx-backed sheet blocks it with the same gate the canvas uses.
- Asset cells have a picker and take drops. An
AssetRef@Groupcell is a text box plus a ⊙ button, in the spirit of Unity's object field. Whichever way an asset arrives, the rules are the same.- ⊙ opens the SheetForge asset picker: the addresses already in that group — narrowed to the column's
<Type>when it has one, sub-assets listed after their parent asparent[sub]— with search and the current value marked. A ⊙ Browse project… line under the list opens Unity's own object picker filtered to that type. - You can also drag assets from the Project window onto the cell: one asset for a scalar column, any number for a
List<>(appended in order; a key already in the cell is skipped). - An asset already registered in that group keeps its address. An asset that is not addressable yet is staged for registration: the cell gets its address at once — the file name without extension — and the group entry, and the group itself when it does not exist yet, are created when you reflect, after the sheet write has succeeded. Nothing in the project changes before that, and discarding the staging changes nothing at all.
- An asset registered in another group asks first — a dialog naming the two groups and how many other cells reference it through the old one — and then stages a move.
- Refused, with a toast saying why: scene objects, folders, several assets dropped on a scalar cell, an asset that does not satisfy the column's type, and an address already used by a different asset in that group.
- Staged registrations appear in the push modal under Addressables and count toward the topbar pill.
Ctrl+Zremoves the cell value and the registration together; retyping the cell so that nothing references the address any more drops the registration in the same step, so a stale registration never survives on its own. - Every asset cell also carries a standing ↗ button on its right: press it and the Project window pings and selects that asset — a list cell selects every asset it holds and pings the first, and a
parent[sub]address pings the sub-object itself. Ctrl+click (Cmd+click on macOS) on the cell does the same, while a plain click still edits the text. When the address does not resolve to an asset the button stays visible but disabled, with the reason as its tooltip. - Right-click a cell for Ping in Project — covering list cells too — and, on an optional column, Clear (default). The canvas's value rows carry the same ping button, because they are the same widget.
- Problems reports
UnknownAssetKeyandAssetTypeMismatchbefore you reflect, and a staged registration is not an error — the projection already counts it. Without the Addressables package the ⊙ button and the drop are disabled with the reason as a tooltip; typing an address still works.
- ⊙ opens the SheetForge asset picker: the addresses already in that group — narrowed to the column's
- Asset cells show the asset itself. When an
AssetRefcell's address resolves, a small thumbnail stands beside the text — any asset type the editor can render a preview for, sized so the row height never changes; aList<>cell shows its first element's.- Click the thumbnail and a floating enlarged view opens; on the record canvas, image assets are drawn large on the card.
- An address that does not resolve simply shows no thumbnail — the ↗ ping button already carries the reason.
AssetRef<AudioClip>cells play. A play button beside the clip's address plays it through the editor's own preview channel; pressing it again stops, and so does moving on — switching cells, closing the window, a domain reload.- When the editor build cannot provide the preview facility the button stays visible but disabled, with the reason as its tooltip. On a cell whose asset is not an audio clip there is simply no button.
- Colour, curve and gradient cells are the native fields. A
Color,AnimationCurveorGradientscalar cell is Unity's colour field, curve field or gradient field — the whole cell, no raw text.- Every change commits the canonical text (see Sheet Syntax). A burst of changes — dragging in the colour picker, a curve-editor session — coalesces into one undo step, the same rule typing follows.
- On an optional column an empty cell shows —. Click it to open the field at the type default, and right-click ▸ Clear (default) to empty it again.
List<Color>,List<AnimationCurve>andList<Gradient>are chip editors: one chip per element (a swatch, a curve thumbnail, a gradient bar) and a+chip. Clicking opens a popup with a native field per element plus ▲▼ and ✕; add, remove, reorder and each element edit are each their own undo step, because a list's order is data. Lists of other types stay canonical text.- The canvas value rows use exactly the same widgets.
- Restrict an asset column to a type from the column form. In the column-properties form — and in the add-column and create-sheet forms — a Type… button always stands beside the
@typefield; it is enabled while the notation being typed is anAssetRef@Group, and otherwise stays disabled with a tooltip that says why. It lists the asset types the project can load (searchable, short name with the full name as subtitle) and rewrites the field toAssetRef@Group<Type>, leavingList<>,?and=defaultuntouched; No restriction (any asset) removes the<…>. Typing the restriction by hand works too. The column-header tooltip shows the resolved full type name. - What you type is what you see, immediately. Grid text is the baseline plus your staging, so it never waits on validation. A committed value shows its staged colour the moment it commits — a value picked from a dropdown, a visual-value edit and fast typing alike, even while the cell still has focus. The heavier preview (Problems, counters, chips, canvas) recomputes once per editing burst instead of once per keystroke. The exception is a one-shot gesture on the canvas, which redraws on the spot.
- No inline validation as you type — diagnostics come from one channel only (the pre-flight/import validator), shown in Problems.
- + Row adds a record with a suggested id (common-prefix detection, collision-safe). Every staged change — cell edits, key edits, row adds, discard-all — is a Ctrl+Z / Ctrl+Y step. Closing the window with staged changes prompts save/discard/cancel.
- ↑ Push to source runs the shared dispatcher: pre-flight → per-source write-back → automatic re-import. Key renames still ask "N references will be updated — continue?", and a Google push still shows the plan approval before anything is sent.
- Honest gates:
- a read-only source (Google ExportUrl) disables editing and push with a mouse-over reason;
- an xlsx-origin tab is read-only with a badge (write-back skips xlsx);
- while an import/export/push is running, pull and push are disabled.
Selecting rows and columns — and dragging them
The grid's coordinates are handles, the way they are in a spreadsheet.
- Click a row number to select the row; click a column letter to select the column.
- Ctrl+click toggles more of them into the selection; Shift+click takes the span from the last click.
- The header and the body highlight everything selected, and the cell the keyboard sits in draws a selection outline that follows arrow-key moves.
- Switching sheets drops the selection — the same numbers would point at different columns there — and the menus carry a Clear selection item.
- Drag a selection to move it. Grab a selected row number (or column letter) and drag: an insertion line tracks where the drop would land, and dropping moves the selection there as one
Ctrl+Zstep.- A non-adjacent selection — rows 2, 5 and 9, say — arrives gathered at the drop point in the order you selected them.
- While a sort, search or filter pins the visible order, row moves stay refused for the same reason the menu items disable themselves — and starting the drag says so the moment it would begin, instead of silently not moving.
- The right-click menus act on the whole selection. With several columns selected, Column width…, Fit to data, Hide, the ◀▶ moves, delete and Clear column data… apply to every selected column; with several rows, the moves and delete cover them all — each batch as one undo step.
- Items that only make sense for one target at a time — properties, filter, sort, freeze, insert — stand apart in their own single-target section.
- A blocked item stays visible with its reason, as everywhere else.
Selection is a view-time gesture: nothing about it is staged or written until an action uses it.
The menus themselves are one component now. Every context and dropdown menu in the Studio — column headers, row gutters, the toolbar's ⋯, the sidebar, canvas nodes and wires, cell menus — has the same shape: titled sections, check marks, keyboard navigation, and disabled items that stay put with the reason. It is the same shape the web app's menus have, so nothing needs relearning between the two.
Editing on the canvas
The canvas edits the same staging buffer as the table. Everything below is an ordinary staged change: it shows in the push modal, it is one Ctrl+Z step, and it is written back by the same pre-flight and re-import chain.
What is drawn
Everything flows one way: materials on the left, the things that use them on the right, with the record you opened in between.
A segmented control above the canvas picks how far each way:
- materials → this → used by — one step each;
- materials only — two steps, for following an assembly chain;
- used by only.
A sheet with no reference columns of its own has nothing to its left, so it offers the last of those alone. The step limit exists so one widely-used material cannot pour the whole project onto the screen. To see further, double-click a neighbour to make it the new terminus.
- The same record reached twice is still one node.
- A record that does not exist is drawn anyway, marked broken, because a link you cannot see is a link you cannot fix.
- A cycle is not hidden either: the edge that closes it runs back along a lane under the cards, dashed, so you can follow where it returns to.
The layout is deterministic — the same data always produces the same picture. Node positions can be dragged for the session, and dragging is deliberately not saved.
What is on a node
The header carries the key, a subtitle (its sheet, or the human-readable name a plugin supplied), the cell address, and a badge counting who else points at this record.
Below it the reference ports are always visible. A port is where a wire plugs in, so hiding one would make the picture lie. Each port shows a filled or empty dot, what it accepts, and how many are linked. A list port shows its elements in order, each with move and remove controls, because a list's order is data.
The plain columns fold into an "n values" group, open on the terminus and closed elsewhere. Each row is a value row you can type in — the same widget the grid uses, committed through the same gate. Port dots are tinted with the colour of the sheet they accept, so one chain reads as one colour.
A card shows the same projection the table shows. A column staged in this batch — not yet reflected, required and reference types included — already stands on the card, marked as pending the way the table's header marks it, tooltip included: a staged reference column appears as a port, a staged plain column as a value row, a staged rename shows the new name over the original value, and a staged delete removes the row from the card too. Adding a required column to a populated sheet does not blank the cards while the empty cells are still being filled — the canvas keeps showing the sheet as it will be after the reflect, exactly as the grid does.
On the right shoulder is a single output dot. What this record exports is itself, so there is one. Dragging from it asks the opposite question ("where do I use this?") from dragging a port ("what does this point at?").
That dot is drawn only when some sheet in the project actually has a column that accepts this sheet, because a dot with nowhere to go is an affordance that is guaranteed to fail. The test is the schema, not the data: a column that declares RecordId@ThisSheet keeps the dot alive even before anyone has written a single row.
Gestures and menus
The dot is the handle. Pressing a port dot starts the gesture and pressing anywhere else on the row does not, so nothing is grabbed by accident. Press and release without moving and it is still a click.
- A card's header is its own handle:
- click it to select the card, which also raises it above any card overlapping it;
- drag it to move the card for this session;
- double-click it to make that record the new terminus.
- Right-click empty canvas → New record. A cascading picker opens: the left pane lists the sheets the ports currently drawn aim at, the right pane carries search, create and the record list, and a third page asks which port receives the record when more than one could.
- A plugin can narrow or widen that sheet list. A sheet it declared that no port on screen accepts stays listed with its reason attached rather than disappearing.
- There is no context-free "new record" here — every record born on the canvas is born linked. A standalone row is what the table's + Row is for.
- Click an empty port dot, or right-click a node → Connect, to link that column. The picker opens at the cursor and is the same searchable key list the grid uses.
- Two header lines say what the slot accepts and which cell the pick will be written into.
- Each row carries the key, its display name and how many places already point at it, and keys already linked here are marked rather than silently duplicated.
- If the record you want does not exist yet, the search box becomes a key box with the sheet's common prefix filled in. Type a name, press Enter, and it is created and linked in one gesture that one
Ctrl+Zundoes. A name that is empty or already taken says so (and where) instead of creating anything.
- A record you create on the canvas becomes the canvas. The view routes to it the way double-clicking a card does, so the new record stands in the middle with the link you just made beside it. (Forking is the exception — the point of a fork is that this terminus keeps its own copy, so the view stays where it is.)
- Drag from an empty port dot and the wire follows the cursor: drop it onto a card to link, onto empty canvas to open the picker there.
- Dragging from the output dot goes the other way. Release over a card and the canvas finds which of its columns can take this record — one candidate links straight away, several ask which. Dropping on empty canvas opens the mirror cascade of sheets that could take it, including sheets not on screen.
- While a drag is live everything unrelated dims and the ports that could accept it glow, so the legal targets are visible rather than guessed.
- Every completed gesture answers with a short toast naming the cell it wrote — and, when it matters, that the link closed a cycle or that the record is now shared with someone else.
- Pick up a link that is already plugged in. Dragging a filled single-reference port lifts that very wire off, exactly like pulling a cable out of a socket. Drop it on another card to re-aim it, or on another port to move the link across two cells — still one undo step, and neither cell is written unless both plans stand. Drop it back where it was, or press
Esc, to change nothing.- Dropping it on empty canvas opens that cell's picker at the cursor, with disconnect offered as a line inside it rather than done on the spot: a hand that slipped must not erase a link.
- Clicking such a dot without moving selects that wire instead, so the two gestures never cost each other anything.
- A filled scalar port is still a valid target for someone else's drag — dropping onto it means replace.
- List ports have no single wire to lift, so they always start a new link. Removing or reordering one element is what the element's own ✕ / ▲▼ row is for.
- A wire's body is for choosing, not for dragging. Click anywhere along a wire — the clickable strip follows every segment of its path — to select it and open the wire inspector.
Deleteor right-click ▸ Disconnect unlinks it. Re-aiming is the port gesture above, so there is exactly one way to do each thing. - Right-click a node → Delete this record. One dialog names everything that goes: how many connections point at the record, and how many staged edits on that row will be dropped.
- Fork a shared record. When a record is used in several places, the node inspector offers Duplicate and split. It clones every column into a new key (suggested from the sheet's prefix), and re-aims only this canvas's links at the copy. One undo step, and the record everyone else uses is untouched.
- The wire inspector opens with the selected wire. It says one line is one token: the cell the link is written in, and that cell's whole text split into its tokens with a mark on the one this line is.
- When the plugin that opened the edge describes it, one row edits the part of the token that is not the key (the
add:10ofattack:add:10). Editing that row rewrites the leftover only; changing what it points at is re-aiming, which is the port gesture. - From here you can also open the record it points at, or cut the link.
- When the plugin that opened the edge describes it, one row edits the part of the token that is not the key (the
- The
Deletekey reads the selection, and asks what that selection means here.- With a wire selected it unlinks that wire.
- With a card selected it depends on whether the card is the record you opened. On the terminus — the card in the middle, the record this screen is about — it deletes the record, through the same confirmation dialog as the menu item.
- On any other card it cuts every connection between that card and this screen at once, because from here that card is material, not the subject. That is one undo step, with the plans chained so a list column loses all of its matching elements rather than all but the last, and a connection it could not cut says why rather than going quiet.
- (Right-click ▸ Delete this record always means delete, whichever card it is on — the key is the shortcut whose meaning follows the selection, the menu line is the explicit one.)
- A card a plugin drew that is not a sheet row has nothing to delete or unlink, so the key does nothing on it.
- A new record that is linked to nothing is not drawn on someone else's canvas. A new row shows up here only while a staged connection ties it to something on screen — otherwise a single unfinished row in the table would follow every canvas in the project around.
- Nothing is lost by that. Creating a record from the canvas immediately re-routes the view to it, so the row you just made is what you are looking at. A row made in the table stays in the table. And a row that ends up linked to nothing is named in the reflect dialog's discard list before it is dropped, whether it was ever drawn or not.
- Ctrl/Cmd + wheel zooms the canvas between 25 % and 200 %, holding the point under the cursor still. The percentage in the canvas header is a button that snaps back to 100 %. A plain wheel still scrolls.
- Drag with the middle mouse button — or Alt + left button — to pan. The cursor changes while you hold it, so it is clear the view is being moved rather than something on it.
- Left-drag alone keeps meaning "select and link", which is why panning got its own button. Alt + left is the same gesture for mice and trackpads that have no middle button.
- Panning stops at the edge of the content: this is a scroll view, not an endless plane, so you cannot lose the graph off in blank space.
Disconnecting and refusals
Disconnecting says what will happen before you press it. Clearing is never refused, but the menu line spells out the consequence:
- an optional column with a default falls back to that default, so the wire goes and the record still resolves at runtime;
- an optional column with no default simply becomes empty;
- a required column becomes an empty cell that Problems reports — you can stage it, and pre-flight will stop the reflect until you fill it;
- a list simply loses that one element.
Validation lives in one channel; the canvas never runs a second one of its own.
Two things are refused rather than faked, and the menu says which:
- A wire that does not say which cell it is written in cannot be edited. The core fills that in for the references it reads itself; a plugin that adds a wire by hand supplies it (or honestly leaves it out).
- A link buried inside a custom type — the stat name inside a
Modifiervalue, for example — is only editable if the plugin that opened that edge also opted intoIAuthorableEdgeContributor. Otherwise the wire is display-only.
Duplicate keys, read-only sources, xlsx-backed tabs and a running import disable canvas authoring exactly as they disable the table's, each with its reason in the menu line.
Structure editing
Everything below is staged like a cell edit — visible immediately, undoable, confirmed by one write-back. The affordances live in the right-click menus described above (column header · row gutter · sidebar sheet).
The Studio also keeps standing buttons on the sheet's header bar, next to the sheet name, so the most-used ones can be found without knowing the right-click menus exist. Among them are + Row and + Column, which appends a column at the end. The same bar carries the sheet's rename ✎, the @overlap row toggle, and the search and sort controls. Right-clicking a header is still the way to put a new column at a particular position.
- Columns: add, remove, move, and edit properties in place.
- The property form (properties ✎ on the header) edits name,
@type,@desc, and@overlaptogether. Only the marker cells change; the column's data cells are 100% preserved (no delete-and-re-add data loss). @overlapis a checkbox. The row is auto-created when first needed, and the box is disabled on key columns, which are always unique.@typeinput is validated as you type. The pre-flight then verifies every existing cell is compatible with the new type before anything is written.
- The property form (properties ✎ on the header) edits name,
- Markers: add/remove the optional
@overlaprow. A sheet's own@stylegroup label and colour are staged through the same path (sidebar right-click ▸ Group & color ✎), so grouping and colouring are pushed to the sheet like any other edit rather than kept in editor-local state. - Rows: delete; reorder with ▲▼ (in the Studio, "move up" / "move down" in the gutter menu).
- Column reorder — any number of moves, confirmed in one reflect. Pending (newly added) columns reorder too, all the way to the front, past existing columns.
- Comments and blank rows are preserved through all structure edits.
WYSIWYG: the grid is a live preview of the post-reflect sheet
Everything you stage is immediately what you see:
- An added column appears instantly — and you can fill its cells in the same batch (existing rows and new rows alike). This is how you add a required column to a populated tab: add it and fill the values together. Unfilled required cells are caught by pre-flight.
- Deleted columns/rows disappear instantly. The staging summary lists what is pending (
Deleted: column 'hp'·Added: column 'mp') — display only. - Everything mixes freely in one batch: value edits, row adds, multiple column adds/removes, marker toggles, and reorders — no "one operation per batch" restrictions.
- Deleting a column/row that already has staged edits (or canceling a filled new column) asks "N edits will be lost — continue?" — nothing is discarded silently.
Tab rename & A↔B swaps — references follow the data
Tab rename ("Rename tab ✎") renames the source and updates every @type reference to the tab across all sheets (RecordId@Foo → RecordId@Bar, list and custom forms included), after a confirmation dialog. Renaming the source means a file rename with the extension preserved on local, and a title change with the content untouched on Google.
Mutual swaps (A↔B) and cycles are supported in one batch: staging either half first works, and references follow the data through an atomic swap rewrite.
- Local exchanges the file contents in one write pass and cleans up stale old-extension files.
- Google orders the title changes topologically, using a temporary title to avoid any momentary duplicate.
Re-import handles the cleanup automatically: generated class rename, old-class file removal, and baked SO and Addressables address re-registration.
Key (RecordId) rename with reference propagation
Edit a key cell directly in the grid. On reflect, every cell in every tab that references that key (RecordId@ThisTab scalars and list elements) is updated together, after a "N references will be updated — continue?" confirmation. Works on both local and Google paths. Boundaries:
- Propagation touches baseline cells only. Text you just typed in the same batch is never silently rewritten; pre-flight catches the dangling reference instead so you fix it knowingly.
- If propagation would touch an xlsx-origin tab, the whole reflect is blocked, because partial reflection is never allowed. Edit that workbook directly and re-import.
- The secondary integer key (
IntId) is editable the same way.
Ctrl+Z / Ctrl+Y — wired to Unity's native Undo
Every staging change integrates with Unity's native Undo:
- cell edits;
- key edits;
- column add/remove/cancel;
- row add/delete;
- marker toggles;
- reorders;
- property edits;
- tab renames;
- canvas links and unlinks;
- asset drops, together with the registrations they stage;
- even "discard all".
Two boundaries to know:
- While typing in a text field, the field itself consumes Ctrl+Z first (standard Windows behavior).
- After a successful reflect, the staging history is cleared — Ctrl+Z moves within the pre-reflect session only. Changes already written to the sheet are undone by editing the sheet.
Staged edits survive re-imports
Staged edits carry a logical address (tab · row key · field), not a physical row number, and are re-resolved just before reflect. So your staging survives automatic re-imports and background baseline changes.
An edit is isolated when the sheet changed externally in a way that breaks resolution — the row's key was renamed outside, the row was deleted, or a key now conflicts. An isolated edit is excluded from the reflect and shown with an "isolated" badge. It is neither silently dropped nor allowed to block the rest of your session; you can discard it individually after confirmation.
Pre-flight validation — validate, then reflect
Writing back first validates the entire projected state in memory — reference integrity, @overlap uniqueness, asset keys and asset types (with the registrations staged in this session counted as present), plus any plugin domain validators. If validation fails, nothing is written and you get the full report. The write starts only after the whole projected state passes; the staged Addressables registrations run after the sheet write succeeds and before the automatic re-import, and a reflect whose only staged change is a registration still runs it and re-imports (see Sources, Export & Push).
Sheet create / delete
- Create from one of the two built-in templates, from a plugin-registered template, or from scratch by defining fields. One click creates the local file or Google tab (after a warning).
- The built-in templates are Item example (core types only) and Enum definitions, which lays out an
@enumsheet (see below). - A plugin-registered template is present only when a plugin provides one — e.g. the Plugin Demo's skill tabs.
- The built-in templates are Item example (core types only) and Enum definitions, which lays out an
- If you create into a source other than the one you are currently importing from, the dialog says so before it acts. The automatic re-import would read the old source and never see the new tab, so creating switches the import source to the one you created into and saves that to the settings asset.
- Delete cleans up everything together: source, baseline, baked SO, and generated
.cs(strong warning + approval).
Cross-sheet search (the All entry)
The first item in the sidebar is All: type a term and it searches every sheet's ids and field values by partial match, listing sheet · key · field · value, 50 rows to a page. With the box empty it browses every record in every sheet.
It reads the baked databases, so it needs one successful import first and says so plainly when there is none yet. The results refresh themselves when an import completes or the active settings change, rather than showing a stale answer.
A result row is a door, not just a listing. Double-click it — or select it and press Enter — and that sheet opens with the matched cell selected; a single click only selects the row. Because the results come from baked data, a result can name a sheet the current session has not loaded — that row does not navigate, and a short notice under the list says why instead of doing nothing.
Opening a sheet that failed to import (quarantine)
A validation error used to leave you with nothing to fix it in: the import failed, so no sheet appeared in the window, and the only way forward was to open the spreadsheet itself. That is inverted now.
Fetching and saving are separate from validating. As long as the source was read completely, the sheets are saved as the baseline snapshot even if validation failed — so the Studio opens them, shows every error against the offending cells, and lets you fix and push exactly as usual.
What stays blocked is the output: code generation and baking do not run until the error count is zero, so "no partial assembly" is preserved where it belongs — at the exit.
- You are told, not left guessing. The console says the sheets were saved and what is blocked. The sidebar carries a
⛔ N error(s) — code generation and baking are blockednotice that jumps to the first error when clicked, and per-sheet error badges come from the projection diagnostics you already see in Problems. A sheet that could not be parsed at all says so instead of appearing empty. - Pushing your fix asks once. While the sheet is quarantined, writing back shows an extra confirmation: the existing errors stay, and a new error this edit introduces is written too — validation will not stop it. Everything it finds is listed as a warning in that reflect's report, and the automatic re-import re-validates the whole sheet afterwards. On a healthy sheet nothing changes: pre-flight is still a hard gate.
- The exits stay shut. Export and Push to the live sheet are both refused while quarantined, because both splice the last successfully baked values into the newer sheets — which would roll back the very values someone just fixed. A build is stopped by the freshness check for the same reason, and the Getting Started window shows the state too.
- The flag is cleared at exactly one place — the moment an import runs all the way through bake. No earlier step can clear it, so a later failure cannot leave a false "all clear" behind.
Sort and filter (display only)
The Studio adds a per-sheet sort (any column, ascending/descending, type-aware comparison, persisted per project) and a text filter. Both change the display order only: the gutter keeps real sheet row numbers, and nothing about sorting or filtering touches staging, reflect, push or export.
While a sort or filter is active the row-move items in the gutter menu disable themselves and say why, because "visible neighbour" would not be the real neighbour. The per-column value filters and column hiding described below follow the same rule.
Frozen columns (display only)
Right-click a column header ▸ Freeze up to this column and that column, plus every column left of it, moves into its own pane outside the horizontal scroll — so the key stays in view while you scroll out to a far column. Unfreeze columns releases them. At least one column has to stay scrollable, so freezing the last one is refused with the reason shown. A table with a single column has nothing to freeze.
What is frozen is a count of columns from the left, not a particular named column. So moving a column changes which ones are held, exactly the way frozen panes behave in a spreadsheet.
Like sort and filter it is a view setting, stored per sheet in per-project preferences. It never reaches the sheet, so it works on read-only and xlsx-backed sheets and while an import is running, and it appears in no push or export.
Column width, hiding and per-column filters (display only)
Every column — on data sheets and enum definition sheets alike — can be sized by hand: drag the handle at the right edge of the column's letter cell, double-click the handle to return to the measured fit, or use the column menu's Column width… for an exact number and Fit to data to measure again. The floor is 24 px and there is no upper cap. Widths are stored per sheet and restored when the sheet or the editor comes back, and the automatic fit measures every row — not only the visible ones — so a filter cannot make widths jump.
The same column menu carries the rest of the spreadsheet set:
- Hide column / Show hidden columns (n) — hiding is a view setting: the letters keep naming the sheet's real positions (A · C · D), a bold boundary line in the header and the body marks where hidden columns sit, and a toolbar chip counts them. The last visible column refuses to hide, with the reason. Hidden columns still exist everywhere else — the canvas, validation, Export and Push see the sheet unchanged.
- Filter… — a checklist of the column's values (with a (Blanks) entry for empty cells) plus one condition: contains / does not contain / is empty / is not empty on any column, and equals / does not equal / greater / less / at least / at most on number columns. With nothing checked every value passes and the condition still applies. A filter combines with the search box, newly added rows always stay visible, the toolbar chip counts the filtered columns, and Clear filter removes one.
- Clear column data… — after a confirmation, empties that column's value in every row (filtered-out rows and staged new rows included) as one undo step. The key column refuses — each row's name lives there; rename it in its cell — and a required column honestly reports errors until refilled. A column with nothing to clear stages nothing.
- Sort ascending / descending / sheet order — the per-sheet sort, now also in the column menu with a check mark on the active choice.
Like sort and frozen columns, all of it is display-only: per-sheet view settings that never reach the sheet, a push or an export.
Enum definition sheets
A sheet that defines enums (see Sheet Syntax) has no records, so the Studio gives it its own grid instead of the record table. The grid shows one column per enum with its members listed in order, an Enum definitions badge in the sidebar, and a count of how many enums it holds. It reads like every other sheet: column letters across the top, real sheet row numbers in the gutter, the same right-click menus in the same order, and staged changes marked with the same colours the data table uses.
The sheet's structure is authored in place, exactly like a data sheet's:
- Define a new enum with the + Enum header-bar button (the only way to put the first enum on a definition sheet that has none yet) or from a column header's right-click menu at a chosen position. The form takes the name, the optional underlying type and description, and the first members — at least one, because an enum with no members cannot be imported. The members' order is their integer values.
- Rename an enum, and every
@typecell that refers to it across all sheets is rewritten in the same batch, so the re-import after the write still resolves. Edit the underlying type and description in place — the marker rows are shown even before the sheet has them, and are created when the change lands. - Delete an enum — refused while any sheet still uses it, with the using columns named. Reorder columns freely; an enum's column position carries no meaning, so this is always safe.
- Everything is staged like any other edit: visible immediately, one
Ctrl+Zstep per gesture, written by one reflect. Every ruling — is the name legal or taken, is a member valid, is the underlying type allowed, is the enum still in use — is made by the same code that validates the import, so the form and the re-import cannot disagree. A blocked action stays in its menu with the reason instead of disappearing.
Member rows still only append and there is no sorting, because a member's position is its value — rearranging members would renumber data already baked into assets.
Cells show exactly what the sheet says: a member pinned with the Name=value syntax appears as written. Duplicate checks compare the name part only, so staging Rare onto a sheet that already holds Rare=10 is refused rather than doubled.
To add a member you do not even have to open the sheet. An Enum<T> cell's dropdown anywhere in the project carries "Add a new member…" at the bottom: type the name and it is staged onto the enum sheet as one Ctrl+Z step, marked as staged until the next reflect writes it.
The row is absent — rather than present and failing — on an enum a plugin registered from C#, because the sheet does not own that one.
The web app authors these sheets the same way, with the same rules — see SheetForge Web.
Jump from a Console error to the cell
When an import or reflect finds errors, the human-readable Console report appends a clickable "Open in Data Studio" link next to each error's coordinate. Clicking it opens the Studio, switches to that tab, and highlights the cell (a left bar on the row and a border on the cell). Errors without a specific cell (file/tab level) just focus the tab.
The link is added only to the human-readable report; the machine-readable coordinate line that CI and log scraping rely on is left untouched.
Health check
⋯ ▸ Health Check in the Studio toolbar runs a quick, no-network diagnosis. It prints ✓/✗ with a suggested fix for each of:
- active settings;
- source reachability — a local folder that exists, or a Google id + service-account key path;
- an import baseline;
- generated-code/baked/addressable freshness.
A summary dialog tells you how many checks need attention.
Active import settings
When the project has more than one SheetForgeSettings asset, the toolbar shows a dropdown to choose which one is active. Selecting one activates it immediately and rebuilds the view; menus, imports and the window all follow the active choice. With a single settings asset the dropdown is hidden.
The active choice is stored per project and per user, so it never touches version control. You can also manage it from the Getting Started window (Tools ▸ SheetForge ▸ Getting Started) or from a settings asset's inspector, which shows an "active" badge or a "Set as active" button.
Safety nets
- External-change detection for local sources.
- A close warning when staged changes exist.
- Staging-generation anchors that guard against acting on a stale grid.
- Mode awareness: with a read-only source (Google ExportUrl), Push / reflect / structure edit / delete are disabled with a mouse-over explanation.
- Progress bars during import/export/push, always cleared (
try/finally), localized. - A misbehaving extension cannot take the window down: canvas overrides, widgets, and actions are called inside try/catch, and a failure is an English console warning while the rest of the window keeps working.
Things to know before renaming (not reversible after reflect)
- Column rename /
@typechange regenerates the generated field (name/type), so game code referencing that field must be updated by hand. The confirmation dialog says so explicitly, and a resulting compile break is safe-aborted by the auto-chain with an actionable sentence. Column values are preserved. - Tab rename changes the generated class name (
FooDatabase→BarDatabase) — same rule. - Mutual swaps and cycles follow the same rules as single renames (see the tab-rename section above). Only a real conflict — two renames to the same name — is rejected.
- xlsx-origin tabs are excluded from rename. The form says why and Apply is disabled; rename the sheet in the workbook itself, then re-import.
- On Google, one edge remains: two swapped tabs that reference each other aren't repointed (local is fully correct) — see Capabilities & Limits.
- After reflect, these cannot be Ctrl+Z'd (the sheet has been written).
Test edit (temporary SO tweak)
The baked SO inspector has a "test edit" toggle (default off = read-only). Toggle it on to temporarily adjust SO values and try runtime numbers — never written to the sheet, gone on the next re-import. Records are quick-findable by codeName/id, in the editor and in Play mode.
The Workbench's separate "preview my staged values on the SOs" button went with that window. The overlay it drove is still public API for a tool that wants it: EphemeralSoApply applies what it can truthfully compute, and declines pending columns and parse-failure cells rather than faking them. The toggle above covers the everyday case.
With and without plugins
Everything listed as Plugin-free (core only) works on a plain project. Each With a plugin line is what a domain plugin adds. All of it is exercised by the bundled Plugin Demo, so you can see each one running before writing your own (Plugin Authoring).
- Sheets, cells, rows
- Plugin-free: full table, cell editing, row add, staging, Ctrl+Z, push.
- With a plugin: unchanged.
- Pull / Push
- Plugin-free: import from the active source; push through the same pre-flight + re-import chain.
- With a plugin: unchanged.
- Problems
- Plugin-free: the four core validations (keys, references,
@overlap, asset keys). - With a plugin: plus your
IDomainValidatorrules. The demo warns on an unbraked reaction cycle and on an orphan record.
- Plugin-free: the four core validations (keys, references,
- What the canvas shows
- Plugin-free: the record you opened, what it is made of and what uses it, laid out deterministically.
- With a plugin: plus your per-tab override via
ISheetForgeGraphPlugin— identities that are not sheet records, links the core cannot see, columns pinned by domain order (absolute or counted from the record on screen), and the human-readable name a record goes by. The demo grows event nodes and a code block around anExampleActionsrecord, pins the cast stages of anExampleSkillsrecord, and turnsfireballinto Fireball on the card and in the picker.
- Edges drawn
- Plugin-free: every
RecordId@Tabreference the core scanner sees (scalars, list elements, defaults, wrapper innards). - With a plugin: plus edges declared by an
IEdgeContributor, and any extra edge a canvas override reveals. The demo opens the stat reference buried inside aModifiermini-syntax value.
- Plugin-free: every
- Editing on the canvas
- Plugin-free: create, connect, disconnect, re-aim, fork, delete and inline value editing for every column; new records can be created in the sheets the drawn ports aim at.
- With a plugin: plus
IAuthorableGraphShapeto decide where new records may be created,IAuthorableEdgeContributorto make a contributor's own edge editable, andIEdgeTokenEditorto expose the part of its token that is not the key. The demo's contributor rewrites themodifiermini-syntax when you link or unlink a stat, and offers itsop:valueas one editable row in the wire inspector.
- Cell widgets
- Plugin-free: the built-in branches (toggle, choice picker, reference picker, asset picker with drop, colour / curve / gradient fields, chip lists for lists of those three, raw text).
- With a plugin: plus your
IStudioCellEditorProviderfor one named cell type. With no widget code at all, aStudioCellEditorHintnames one of seven built-in shapes (dropdown, multi-line, slider, toggle, colour, curve, gradient) for that type instead. A registered widget wins; the hint wins over the core default — including over the built-in colour / curve / gradient hints, when a pack registers a hint under the same type name.
- Menus and panels
- Plugin-free: the built-in items only.
- With a plugin: plus your
ISheetForgeStudioPluginregistrations — verbs at the end of the inspector, row menu, column-header menu, canvas-node menu and topbar ⋯, a right-hand summary panel, and a badge beside a column header. The demo stages a reaction brake on one row, fills a whole column in one Undo step, and summarises a skill's assembly with a cast-time gauge.
- Rich panels
- Plugin-free: nothing.
- With a plugin:
IStudioPanelProviderpaints an arbitrary UIToolkit panel. Registered under the same id as a described panel it wins here, while the browser draws the described one.
- Sidebar
- Plugin-free: your sheets only.
- With a plugin: plus locked
_Refs-style code registries (ISheetForgeCodeRegistryPlugin) shown as a read-only key/label/raises grid; edges pointing at those keys are not drawn as broken.
- Above the canvas
- Plugin-free: nothing (the core ships no widget).
- With a plugin: your
IStudioGraphWidget. The demo shows a fixed stage strip (VALIDATE → CAST → COMMIT → DELIVER → APPLY) filled from the terminus row.
- Node inspector
- Plugin-free: address, used-by list, "Go to this sheet", "Duplicate and split".
- With a plugin: plus your
IStudioInspectorActionbuttons. The demo stagesbrakeSeconds = 0.25on an unbraked reaction (a normal, undoable staged edit).
- Colours and labels
- Plugin-free: the built-in presets plus each sheet's own
@stylecolour. - With a plugin: plus your
ISheetForgeThemePluginpreset andISheetForgeStringsPluginstrings — registered in Core, so the same pack's labels also show up in the browser.
- Plugin-free: the built-in presets plus each sheet's own
Getting started with an empty project
Opened with no active import settings, the Studio shows a "Get started" panel in place of the table — inline buttons to create a settings asset, import the Plugin/Core demo, or open Getting Started. It disappears the moment settings exist.
Faster still: drag a folder onto the window, or a single .tsv/.csv/.xlsx file, whose parent folder is used. After a confirmation it creates an import settings asset reading from that folder and makes it active. If you already have active settings, the dialog says so and offers to create a new one and switch.
What happened to the Workbench
The original IMGUI Workbench window has been removed. It shared the Studio's authoring session, compose/validation engine and reflect path, so nothing behavioural was lost — and the three things it alone offered were moved first:
| Workbench feature | Where it lives now |
|---|---|
| The All tab — cross-sheet search with paging | The All entry at the top of the Studio sidebar (same search, same page size) |
| Drag-and-drop onboarding | The Studio itself takes the drop (above) |
| The Google gid autofill / live tab list panel | The settings asset's inspector, under Google Sheets |
One capability was dropped rather than moved: the Workbench's button for previewing staged values onto the baked SOs. The SO inspector's test edit toggle covers the same need (see Test edit above).
If you have code or notes pointing at it: there is no menu entry to update — it never had one — and the settings inspector, the baked-SO inspector and the Getting Started template gallery all open the Data Studio.
Related pages
- Sheet Syntax — what the property form validates against
- Sources, Export & Push — what "reflect" does per source
- Plugin Authoring — writing the canvas overrides, code registries, widgets and actions the Studio picks up
- Authoring Kernel — the engine this window is built on
- Capabilities & Limits — the full authoring-limits list