Skip to content

Modify

Commands that change the model. They share the mutation lifecycle described in Editing & staging: preview by default, persist with --save, batch with --stage, or write elsewhere with --save-to <path> (which implies --save). --serialization tmdl|bim controls the on-disk format, --force (alias -f) saves despite newly introduced validation errors, --overwrite lets --save-to replace an existing target, --no-sync skips the workspace mirror, and --revert discards the model's staged changes. Without --save, --save-to, or --stage, the change is applied to the in-memory model and rendered, but nothing is written.

Before --save or --save-to writes anything, tx runs the same validation as tx validate. Only errors introduced by this command block the save; existing errors and warnings do not. A blocked save exits 1 and lists the new errors. --force writes anyway and reports the introduced errors. rm --force also bypasses its dependent-reference guard. Set validateOnSave to false with tx config set validateOnSave false to disable this gate; it is on by default. Staged edits are checked when you run tx stage commit. The --force flags on init, connect, and config init retain their command-specific uses.

A TMDL save rewrites only the files whose content changed, so a small edit gives a small git diff. Untouched files keep their bytes, line endings (CRLF checkouts stay CRLF), and M partition indentation. Stale .tmdl files for removed or renamed objects are deleted. Other files in the folder, such as a README, are left alone.

Those shared lifecycle options are not repeated in the tables below.

Saving to Power BI Desktop

When the active connection is a running Power BI Desktop model (localhost:<port>), --save writes the change to the model Desktop has open in memory. The change is live straight away (a fresh tx get or tx query sees it), but Desktop keeps it only until the report closes. Save the report in Power BI Desktop to keep the change. tx prints this reminder on stderr after every save to Desktop, and JSON output reports "persistence": "liveModel".

JSON result

Every mutation command (add, mv, set, rm, replace, format, save, vertipaq --annotate, bpa run --fix, bpa rules ignore) reports the same persistence fields under data:

Field Type Meaning
status string saved, staged, preview (applied in memory only), unchanged (nothing to change), or reverted
saved bool The change was persisted. Always a bool
savedTo string Where it was saved: a folder or file path, or server / database. Present only when saved
persistence string file, liveModel (Power BI Desktop, in memory until the report is saved), or service. Present only when saved
target object {server, database, model} for a remote or Desktop model. model is the friendly name tx connect shows
sync object Workspace mirror sync: {status, target?, warning?}. status is notAttempted, notConfigured, skipped, succeeded, or failed
newValidationErrors int Errors the change introduced, when the save gate measured them

The object path uses a past-tense key (added, moved, removed, set) only when the change was saved or staged. Previews use wouldAdd, wouldMove, wouldRemove, or wouldSet, so a script never reads a preview as done:

{
  "data": {
    "wouldRemove": "Sales/Total Sales",
    "status": "preview",
    "saved": false,
    "sync": { "status": "notAttempted" }
  },
  "diagnostics": []
}

A save to Power BI Desktop:

{
  "data": {
    "added": "Sales/Margin",
    "status": "saved",
    "saved": true,
    "savedTo": "localhost:51234 / 0f1e2d3c-...",
    "persistence": "liveModel",
    "target": { "server": "localhost:51234", "database": "0f1e2d3c-...", "model": "Sales Report" },
    "sync": { "status": "notConfigured" }
  },
  "diagnostics": []
}

A failed or skipped sync includes a warning. A failed sync exits 1 even though the save succeeded.

add — add an object

tx add <path> [model] [options]

The path names the new object (Sales/Revenue, 'Sales'[Revenue]); relationships use Sales[Key]->Product[Key] (many side → one side).

Option Description
-t, --type <type> Object type: Table, CalcTable, CalcGroup, Measure, CalcColumn, DataColumn, Hierarchy, Level, Calendar, CalcItem, KPI, Partition, MPartition, EntityPartition, PolicyRangePartition, Expression, Function, Perspective, Culture, ProviderDataSource, StructuredDataSource, Role, TablePermission, Member, Relationship. Often inferred from a container keyword in the path; data sources always require -t.
--expression <value> (-e) Expression or value for the new object. - reads from stdin.
--set <name=value> Set a property on the new object, e.g. --set formatString="#,0". Repeatable; name=- reads the value from stdin.
--file <file> Read the expression from a file.
--columns <names> Comma-separated columns to create on a new table (Table type only).
--if-not-exists Do nothing and exit 0 when the object already exists.
--mode <mode> Storage mode for the partition: Import, DirectQuery, Dual, DirectLake, Push, Default.
Data-source and partition options
Option Description
--source <provider> Provider name for a ProviderDataSource (e.g. System.Data.SqlClient).
--source-type <type> Connection protocol for a StructuredDataSource (e.g. tds).
--endpoint <address> Server/endpoint address for a data source connection.
--connection-string <cs> The connection string used by a ProviderDataSource.
--source-database <db> Source database for a data source connection.
--source-table <table> Source entity/table name for an EntityPartition.
--source-schema <schema> Source schema for an EntityPartition.
--partition-expression <expr> The M or DAX expression defining the partition's source.
--range-start / --range-end <yyyy-MM-dd> Refresh-policy range for a PolicyRangePartition.
--range-granularity <g> Day (default), Month, Quarter, Year.
tx add "Sales/Revenue" -t Measure --expression "CALCULATE(SUM(Sales[Amount]))" --save
tx add tables/Sales/measures/Revenue --expression - < expression.dax
tx add "Sales/Revenue" -e "SUM(Sales[Amt])" --set formatString="$#,0"

set — set a property

tx set <path> [model] [options]
Option Description
--set <name=value> / -p <name=value> Property assignment, e.g. --set expression="SUM(Sales[Amount])". Repeatable; all assignments are applied together. name=- reads the value from stdin.
-t, --type <type> Disambiguate when the path matches multiple objects.
--strict-refs Fail when a rename leaves DAX references broken.
--no-fix-refs Do not rewrite DAX references to a renamed object; warn instead.
tx set "Sales[Total Sales]" --set expression="CALCULATE(SUM(Sales[Amount]))"
tx set "Sales[Total Sales]" --set formatString="#,0" --set displayFolder=KPIs --save   # one load, one save
tx set Sales --set name=Sales_v2 --save
tx set tables/Sales --set excludeFromModelRefresh=true
tx set "Sales[Amount]" --set summarizeBy=Sum
tx set "Dates[Month]" --set sortByColumn=MonthNo                  # empty value clears it
tx set "Sales[OrderId]" --set isKey=true
tx set "Sales[Total Sales]" -t kpi --set statusGraphic="Cylinder"
tx set "Sales Territory/'Sales Territories'" --set hideMembers=HideBlankMembers
tx set "Sales Territory/'Sales Territories'/Region" --set ordinal=2
tx set Sales/Sales -t partition --set mode=DirectQuery

Translations use translation:<culture>/<property>, where the property is caption (alias name), description, or displayFolder (measures, columns, and hierarchies only). Tables, columns, measures, hierarchies, levels, and the model root (.) can be translated. The culture must exist already; add it with tx add Cultures/<culture> -t Culture. An empty value removes the translation, and tx get reads translations back under the same key.

tx add Cultures/da-DK -t Culture --save
tx set "Sales[Total Sales]" --set translation:da-DK/caption="Omsætning" --set translation:da-DK/displayFolder="Nøgletal" --save
tx set "Sales[Total Sales]" --set translation:da-DK/caption= --save   # remove it

When the edited property carries DAX (for example a measure's expression), text output previews the change with Before:/After: lines and syntax colors. Non-DAX properties, unchanged values, and --output-format json show no preview.

Columns accept every writable scalar property (sourceColumn, dataType, dataCategory, summarizeBy, sortByColumn, isKey, isNullable, isUnique, isAvailableInMDX, keepUniqueRows, encodingHint, lineage tags, and more) — everything tx get shows for the column. An unsupported property name lists the full writable set; enum-valued properties list their valid values on a bad value.

Tables accept every writable scalar property too (isPrivate, excludeFromModelRefresh, excludeFromAutomaticAggregations, alternateSourcePrecedence, showAsVariationsOnly, systemManaged, directLakeIndexingBehavior, lineageTag, sourceLineageTag) — everything tx get shows for the table.

Measures accept every writable scalar property (dataCategory, isSimpleMeasure, lineageTag, sourceLineageTag, and more). KPIs accept description, targetExpression, statusExpression, trendExpression, targetFormatString, statusGraphic, trendGraphic, statusDescription, targetDescription, and trendDescription — address them with -t kpi or a /KPI path suffix.

Hierarchies accept every writable scalar property (hideMembers — Default or HideBlankMembers — lineageTag, sourceLineageTag) — everything tx get shows for the hierarchy. Levels accept ordinal, lineageTag, and sourceLineageTag; address a level with its full Table/Hierarchy/Level path.

Partitions accept description, mode (Import, DirectQuery, Default, Push, Dual, DirectLake), dataView, and queryGroup — which must name an existing query group; an empty value clears it. retainDataTillForceCalculate is calculated-source-only, just as expression is M-source-only, and the set hint omits a source-bound property a partition cannot take.

Relationships accept name, isActive, crossFilteringBehavior (OneDirection, BothDirections, Automatic), fromCardinality and toCardinality (One, Many), securityFilteringBehavior (OneDirection, BothDirections, None), relyOnReferentialIntegrity, and joinOnDateBehavior (DateAndTime, DatePartOnly). Address a relationship by its endpoints:

tx set "Sales[OrderId]->Dates[Date]" --set isActive=false

The endpoint columns themselves stay read-only, and cardinality or active-state edits surface in diff through the relationship's detail line.

Roles accept name, description, and modelPermission (None, Read, ReadRefresh, Refresh, Administrator). Role members accept name or memberName, memberId, and — external members only — identityProvider and memberType (Auto, User, Group); TOM freezes a member's identity once attached, so changing any of these fields replaces the member under the hood, and Windows members get a clear error for the provider fields. Table permissions accept filterExpression and metadataPermission (Default, None, Read):

tx set Readers/user@contoso.com -t member --set memberType=Group
tx set Readers/Customer --set metadataPermission=None

Shared expressions and functions accept name, description, and expression; expressions also accept kind and remoteParameterName, and functions accept isHidden — all of them carry lineage tags:

tx set "Expressions/Environment" --set remoteParameterName=RangeStart

The model root is addressed with .: compatibilityLevel, description, culture, collation, discourageImplicitMeasures, discourageCompositeModels, defaultMode (Import, DirectQuery, Default, Push, Dual, DirectLake), defaultDataView (Full, Sample, Default), maxParallelismPerQuery, maxParallelismPerRefresh, sourceQueryCulture, and forceUniqueNames are all settable, and tx get . reads the whole surface back:

tx set . --set culture=en-US --save
tx get . --query defaultMode

Calculation-group tables accept precedence — a plain table rejects it — and calculation items accept description, expression, and ordinal via their Table/Item path.

Data sources accept description, maxConnections, and — provider sources only — impersonationMode (Default, ImpersonateAccount, ImpersonateAnonymous, ImpersonateCurrentUser, ImpersonateServiceAccount, ImpersonateUnattendedAccount), isolation (ReadCommitted, Snapshot), and timeout (seconds); structured sources accept contextExpression. Connection strings, accounts, and passwords are secrets, so they are never accepted via argv — edit the source file to change credentials:

tx set DataSources/Import --set maxConnections=5

mv — move or rename

tx mv <source> <destination> [model] [options]

Aliases: move, rename.

Renames rewrite referencing DAX automatically; --strict-refs and --no-fix-refs behave as on set.

Display folders. Middle path segments are display folders, so mv moves measures, columns, and hierarchies in and out of folders within their table (nested folders as deeper segments). A destination ending in / keeps the source name. Folder segments are only applied when either path names them — a plain rename never touches the folder the object is in; to move an object out of its folder, write the folder-qualified source. A 3-segment path that matches a hierarchy level keeps its level meaning — use -t, --type when a level and a folder path could collide. Folder changes never affect DAX, so no reference fixup runs for them.

Measures can also move to another table (optionally renaming and picking a folder in the same step) — the classic "consolidate into a measure table" operation. A move rewrites fully-qualified 'Table'[Measure] references to the new home table; unqualified [Measure] references stay valid and are left alone. Columns, hierarchies, and partitions are bound to their table's data and cannot move.

tx mv "Sales/Old Name" "Sales/New Name" --save
tx mv tables/Sales tables/SalesData
tx mv "Sales/Total Sales" "Metrics/Total Sales" --save
tx mv "Sales/Revenue" "Sales/Finance/Revenue" --save     # into a folder
tx mv "Sales/Finance/Revenue" "Sales/Revenue" --save     # out of the folder
tx mv "Sales/Finance/Revenue" "Sales/Margins/" --save    # between folders, keep name
tx rename "Sales/Date" "Sales/CalendarDate" -t Hierarchy --save

mv --save overwrites the source (and syncs the workspace mirror), so it asks for confirmation; pass --yes to skip the prompt in scripts. --revert (drops staged work) asks too. Plain mv stays in memory, --save-to writes a copy, and --stage defers the prompt to tx stage commit.

rm — remove an object

Alias: remove.

tx rm <path>... [model] [options]
tx rm --from <file|-> [model] [options]
Option Description
--from <file\|-> Read the objects to remove from a file, or - for stdin (see Batch removal).
--cascade Let a batch also change objects it leaves behind (relationships, sort-by, hierarchy levels, perspectives, role permissions).
--force Remove even if the object has DAX dependents (reports the now-broken references).
--if-exists Exit 0 when the object is already gone; in a batch, skip missing objects.
-t, --type <type> Type to pick when the path matches several objects under a table.

Without --save or --stage, rm previews: it prints Would remove: <path> and exits 0 without touching the model. When the guard would block the removal, the preview lists the dependents (Would break N DAX reference(s) in: ...) and prints the command that removes it anyway (your command line plus --force --save), so the preview is how you discover that --force is needed. rm --save, --save-to, --stage, and --revert ask for confirmation; pass --yes to skip the prompt in scripts.

Removal is blocked while DAX or Power Query (M) still references the object; a shared expression is referenced when an M partition, another M shared expression, or a refresh policy's source or polling query names it (read with the bundled M parser, so field access like [Sales] and names a query binds itself do not count; a query the parser cannot read counts if it contains the name). The preview lists these under Would break N Power Query (M) reference(s) in: ..., and --force removes anyway. Structural references (relationships, sort-by, hierarchy levels, perspectives, role permissions) cascade-remove instead. Every object kind a mutation path can address is removable: tables, measures, columns, hierarchies, levels, partitions, calculation items, relationships, roles, role members, perspectives, cultures, shared expressions, functions, data sources, KPIs, table permissions, and calendars. A data source still bound to a partition cannot be removed until the partition is repointed or removed. A KPI shares its measure's path, so address it explicitly: tx rm "Sales/Total/KPI" or tx rm "Sales/Total" -t kpi (the measure survives; removing the measure takes its KPI with it).

tx rm "Sales/Obsolete"            # preview
tx rm tables/Staging --save
tx rm "Sales[CustomerID] -> Customers[CustomerID]" --save

Batch removal

Give several paths, or list them with --from, to remove objects of any kinds as one change: one validation, one save, and in a live session one undo step. The last argument is still read as the model when it names a folder or file that exists, so tx rm Sales/Old ./model keeps working.

--from takes one object per line. A line is either a tomix object path or a JSON object (JSON Lines):

Shape Example
Path Sales/Old Ratio
{type, table?, name} {"type": "measure", "table": "Sales", "name": "Old Ratio"}
{type?, path} {"type": "column", "path": "Sales/Legacy Key"}

type takes any --type value plus common spellings such as calculatedTable, calculation group, calculatedColumn, and namedExpression. table is required for table children (measures, columns, hierarchies, partitions, calculation items, calendars, KPIs); levels add hierarchy, and role members and table permissions add role. Unknown fields are ignored, so a scanner's own output can be piped in as is. Blank lines and lines starting with # are skipped.

tx rm Sales/Old "Sales/Old Ratio" Staging --save
tx rm --from unused.jsonl                    # preview the whole set
scanner --json | tx rm --from - --cascade --save --yes

Every target is resolved before anything changes. A missing or ambiguous target fails the whole batch and the error names all of them; --if-exists skips missing ones instead. A target inside another target (a column whose table is also removed, a KPI whose measure is) is reported as subsumed and goes with its parent.

The guards judge the whole deletion set, whatever order the targets are listed in:

  • References between targets are fine: a table and its columns, a measure and the measures it calls, a column and the relationship that uses it, a data source and the partitions bound to it.
  • DAX or Power Query (M) in an object that survives the batch blocks it unless --force.
  • A partition that survives and is bound to a data-source target blocks it, --force or not: repoint the partition or add it to the batch.
  • Structural changes to objects that survive (a relationship removed, a sort-by cleared, a hierarchy level or perspective entry or role permission dropped) are listed under Would also change: and need --cascade (TOMIX_RM_NEEDS_CASCADE otherwise). The removed objects' own translations go with them without asking.

A blocked preview prints the command that removes the set anyway, with the flags it needs (--force, --cascade).

The JSON result lists the removed paths under removed (or wouldRemove), then every input under targets, every structural change outside the batch under cascades, and what blocks a preview:

{
  "data": {
    "wouldRemove": ["Sales/CustomerID", "Sales/Order Count"],
    "targets": [
      { "input": "Sales/CustomerID", "path": "Sales/CustomerID", "type": "column", "action": "remove" },
      { "input": "Sales/Order Count", "path": "Sales/Order Count", "type": "measure", "action": "remove" }
    ],
    "cascades": [
      {
        "target": "Sales/CustomerID",
        "path": "Relationships/rel-customers",
        "type": "relationship",
        "description": "relationship 'Sales'[CustomerID] -> 'Customers'[CustomerID]",
        "requiresCascade": true
      }
    ],
    "brokenReferences": ["Sales/Distinct Customers"],
    "reason": "would_block",
    "blockedBy": ["brokenReferences", "cascade"],
    "status": "preview",
    "saved": false,
    "sync": { "status": "notAttempted" }
  },
  "diagnostics": []
}
Field Meaning
targets[].action remove, subsumed (goes with subsumedBy), or notFound (skipped under --if-exists)
targets[].type The resolved kind, as a --type value
cascades[] target caused it; path and type name the object changed; requiresCascade is false only for translations
brokenReferences Surviving objects whose DAX references a target
brokenMReferences Surviving objects whose Power Query (M) references a shared-expression target (also on a single-object rm)
boundPartitions Surviving partitions bound to a data-source target
blockedBy On a blocked preview: brokenReferences, brokenMReferences, cascade, boundPartitions

replace — find and replace

tx replace [pattern] [replacement] [model] [options]
Option Description
--in <scope> names, expressions, descriptions, displayFolders, formatStrings, annotations, all (default; excludes annotations).
-t, --type <type> Only replace in objects of this kind (same vocabulary as ls --type).
--regex Treat the pattern as a regular expression.
--case-sensitive Case-sensitive matching.

Without --save or --stage, replace previews each change as <object>.<property>: <before> -> <after> and writes nothing. The persisting forms (--save, --save-to, --stage, --revert) ask for confirmation; pass --yes to skip the prompt in scripts.

--in expressions walks every expression-bearing property: measure DAX, detail-rows and format-string definitions, KPI target/status/trend, calculated columns, partition M and calculated-table DAX, refresh-policy source and polling M, calculation-group items and selection expressions, role table-permission filters, shared expressions, and DAX functions. --in names covers every renameable object, including role members; table-permission names are excluded because the engine derives them from the table. all deliberately excludes annotations — their values are often tool-generated JSON — so annotations are only touched when requested explicitly (Tabular Editor's CLI includes them in all). Whatever tx find reports for a scope, tx replace rewrites in that scope; a test enforces the pairing.

tx replace "[OrderDate]" "[ShipDate]"            # preview
tx replace "old_name" "new_name" --in names --save
tx replace "Sales" "Revenue" -t measure          # preview

format — format DAX and M

tx format [model] [options]

DAX is formatted offline by the engine bundled with tx — no network, no rate limits, same result air-gapped. DAX output uses the bundled formatter's style (adjustable, see format style): a 65-column prettier-style layout with keywords and known function names upper-cased, so results differ from the daxformatter.com style previous releases produced (and from Power BI's format button). DAX that does not parse is left unchanged and reported with its line and column (DAX syntax error on line 1, column 19: Expected ',' or ')', but found 'Sales'.), with the same caret and syntaxErrors as M below.

Power Query (M) is formatted offline too, by Microsoft's powerquery-formatter bundled inside tx and run in-process — no network, no Node.js, nothing else to install. M is wrapped at 40 columns (120 with --long) with four-space indentation, so results can differ from the network formatter previous releases used. M that does not lex or parse is left unchanged and reported with its line and column (M syntax error on line 3, column 1: ...), counted from the start of the expression. For an inline -e expression, text output also shows the offending line with a caret under the error:

Error: Formatting failed: M syntax error on line 1, column 12: A comma cannot proceed an 'in'
1 | let x = 1, in x
  |            ^^

With --output-format json, an inline or --path failure is a TOMIX_FORMAT_FAILED error whose syntaxErrors array gives the stage, code, and span of each error (see error codes).

With no target, formats every measure (DAX) or every partition (--lang m) in the model. Objects that fail to format are counted in Failed: N and the formatter's error is reported per object on stderr (deduplicated with a (+N more) count when objects share the same failure). With --output-format json, each failed result row carries an error object: message always, plus stage, code, line, column, endLine, and endColumn when the failure is a syntax error:

{
  "table": "Category",
  "status": "failed",
  "partition": "Category-25da50ca",
  "error": {
    "message": "M syntax error on line 4, column 1: A comma cannot proceed an 'in'",
    "stage": "parse",
    "code": "expectedCsvContinuation",
    "line": 4,
    "column": 1,
    "endLine": 4,
    "endColumn": 2
  }
}

If any object fails, nothing is applied, saved, or staged: the run exits 1 with TOMIX_FORMAT_FAILED (No changes applied: N of M expressions failed to format.), and the text summary shows Formatted: N (not applied). The result rows are still written, so the counts show what would change once the failures are fixed.

Format style

Three knobs, modeled on daxformatter.com, set the layout. Each has a flag and a config key; the flag wins for one run, the key (tx config set) for every run, including tx format in tx serve and tx mcp sessions.

Knob Flag Config key Values
Separators (DAX) --separators format.separators us (default): A, B, 1234.00 · eu: A; B; 1234,00
Spacing (DAX) --function-spacing format.functionSpacing space (default): IF ( · none: IF(
Lines (DAX and M) --long / --short format.lines short (default): DAX 65, M 40 columns · long: 120 columns

Separators are display only. Models always store DAX with US separators, so eu rewrites only what is printed — list commas become ; and decimal points in numbers become ,, while strings, comments, and names are left alone. --separators eu together with --save, --save-to, --stage, or --revert is rejected with TOMIX_FORMAT_SEPARATORS_DISPLAY_ONLY; with format.separators=eu in config, a write stores the US form and only the output shows EU. Input must be US-style DAX: tx format does not read ;-separated DAX. Spacing and line length do change what --save stores.

tx config set format.functionSpacing none
tx format -e "IF(x > 1.5, 1, 0)" --separators eu   # IF( x > 1,5; 1; 0 )
tx format --save --long                            # whole model, 120-column lines

Formatted DAX and M are syntax-highlighted in text output, for both inline -e and --path; piping or redirecting strips the color, so the output stays safe to copy back into a model.

Option Description
-e, --expression <expr> Format an inline expression (no model needed). - reads it from stdin.
--path <path> Format the expression on one object.
--lang <dax\|m> Expression language.
--long Long lines: DAX at 120 columns instead of 65, M at 120 instead of 40.
--short Short lines (the default); overrides format.lines=long.
--separators <us\|eu> DAX separators to show (display only).
--function-spacing <space\|none> IF ( or IF( in DAX.
-t, --type <type> Disambiguate --path when it matches several objects under a table.
tx format -e "CALCULATE(sum(sales[amt]))"
tx format --path "Sales/Total Sales" --save
tx format --save                     # whole model
cat measure.dax | tx format          # piped expression

A piped expression is read only when nothing else names what to format: with a model, --path, --save, --save-to, --stage, or --revert, format leaves stdin alone and formats the model. Pass -e - to read stdin anyway.

interactive — edit in a session

tx interactive [model] [options]

Alias: tx shell. Opens the model once and keeps it in memory, then reads commands until exit or the end of input. With no model argument it opens the active connection's model, or starts with no model open when there is none. Every command works as it does on the command line, with its usual flags, and runs against the in-memory model; leave out the model argument and it uses the session's. Commands that need something other than the model (deploy, refresh, query, test, stage, ...) are not available inside a session and say so.

connect works inside a session exactly as it does outside, --recent, --local and --remote included: it sets the active connection, and the session then opens that model in place of the one it has open. Unsaved changes are handled as on exit (asked about at a terminal, refused in a script unless --discard-on-exit or --yes), and a model that fails to open leaves the current one and its changes as they were. connect with no arguments, --list and --clear only show or change the connection.

Edits are kept, not previewed: set, add, rm and the other modify commands change the session's model, and nothing is written until you run save. The prompt marks unsaved changes with *, for example basic-tmdl* >. save is tx save on the session: with no -o it writes back to the source and clears the mark, and -o, --serialization and --fix-bpa work as usual. --save on a modify command applies the change and saves at once. --stage and --revert are rejected (TOMIX_SESSION_STAGE_UNSUPPORTED): undo and transactions take their place.

Inside a session these commands also work:

Command Description
undo Revert the last change as one step. A command that changed several objects (a rename and its reference fixups, a replace) undoes as one step.
redo Reapply the last undone change. A new change clears the redo steps.
begin [name] Group the following commands into one undo step.
commit Keep the open transaction's changes as one step.
rollback Discard the open transaction's changes.
status The model, unsaved changes, undo and redo steps, and the open transaction.
history The changes undo can revert and redo can reapply, labelled with the command that made them.
reload [--discard] Read the model's files again after they changed outside the session. Clears undo history; with unsaved changes it needs --discard.
exit, quit Leave the session.
help List the commands that work in the session.

The session keeps the last 50 undo steps, or fewer on a large model, so that the undo history stays near 250 MB. A command that fails changes nothing.

Files changed outside the session. The session watches the model's files. When something else changes them (a git checkout, a pull, another editor), the session says so before the next prompt, and save fails with TOMIX_SESSION_STALE instead of overwriting that change. Then either:

  • reload takes the files' version. It discards unsaved changes (pass --discard when there are some) and the undo history.
  • save --force keeps the session's version and writes it over the files.

status shows when the files changed.

A model on a server or in Power BI Desktop. save sends the session's changes to the server. The session asks the server whether any object in the model changed (every 10 seconds for Power BI Desktop, every 30 for a remote server). When something else changed it (Power BI Desktop, another tool, or a refresh), the session says so and save fails with TOMIX_SESSION_STALE. The session cannot reload from a server: save --force writes the session's changes anyway, or close the session and connect again to take the server's version (unsaved changes are lost).

If Power BI Desktop closes, or the server cannot be reached, commands that need it fail with TOMIX_SESSION_SOURCE_UNAVAILABLE. Within one check the shell says so before the prompt, status shows it, and the tx ui page shows a banner. The changes are still in the session: save -o <folder> writes them to files. With -o, save only writes a copy and never touches the server.

A session cannot open on a Power BI Desktop that has already closed: it fails with TOMIX_DESKTOP_NOT_RUNNING. Open the report again and run tx connect --local.

Leaving with unsaved changes (or an open transaction) asks first at a terminal. Anywhere else it fails with TOMIX_SESSION_DIRTY and exit code 1 unless --discard-on-exit or --yes says to discard them.

Scripts. Piped or redirected input runs as a script: no prompts, one command per line, blank lines and lines starting with # skipped. The script stops at the first failing command and exits with its code; --no-batch runs past failures and exits with the first failure's code. Global options given to tx interactive apply to every line that does not set them, so --output-format json gives one JSON result per command.

Option Description
--autosave Save after every command that changes the model.
--discard-on-exit Leave without asking, discarding unsaved changes. --yes does the same.
--echo Print each command (to stderr) before it runs.
--no-batch Keep running a script after a command fails.
--no-banner Start without the welcome lines.

At a terminal the session starts with a welcome screen: the open model's name, compatibility level, object counts and where it saves (or how to open one), the keys to know, and a tip. --no-banner skips it. The prompt names the model, tx [basic-tmdl]>, adds * for unsaved changes, and is tx> with no model open. It has line editing, Up/Down history and Tab completion. Ctrl-C cancels the running command, not the session, and Ctrl-D (or Ctrl-Z on Windows) on an empty line leaves.

tx interactive ./model
tx shell                                      # the active connection's model
tx interactive ./model --echo < edits.txt     # run a script; stops at the first failure

An interactive session opens TMDL folders and .bim files.

serve — serve a session to other programs

tx serve [model] [options]

Holds a model session like tx interactive, but for programs instead of a person: editors, the tomix UI and agents send requests on stdin and read answers and change events on stdout, in the session protocol (JSON-RPC 2.0 with Content-Length framing). Each request runs the matching command on the session, so its result is that command's --output-format json payload; edits stay in memory until session.save, and undo, redo and transactions work as in tx interactive.

With a model argument the session opens it at start; without one the client sends session.open. stdout carries only protocol frames. The server's log (one line per request) goes to stderr, or to a file with --log.

Option Description
--log <file> Append the server's log to this file instead of writing it to stderr.

The server exits 0 after shutdown and exit, and 1 when exit comes without shutdown. When the client disconnects (the end of stdin), the server discards unsaved changes, says so in its log, and exits 0. Ctrl+C (or SIGTERM) does the same and exits 130 (143), even while the client keeps stdin open. Requests run one at a time in arrival order; $/cancelRequest cancels one that is waiting or running. initialize lists the methods this version serves: query.run and $/progress are in the spec but not served yet.

tx serve ./model
tx serve --log serve.log

tx serve opens TMDL folders and .bim files, as tx interactive does.

When tx ui already holds the model, tx serve ./model joins that session instead of opening a second one: it passes its client's messages to it, so the client is one more client of the shared session. Leaving (the end of stdin, or exit) then detaches the client and leaves the session and its unsaved changes to tx ui.

ui — share a session with the browser and agents

tx ui [model] [options]

Holds a model session like tx serve, and shares it on 127.0.0.1 so a browser page and agents work on the same open model at the same time: the agent edits through tx serve (or tx mcp), and you watch, undo and save in the page. Every client sees every change as it happens.

Without a model argument it opens the active connection's model. It prints the page's URL, which carries the session's token, on stdout, and records the session in ~/.tomix/live/ so other tx processes find it. A second tx ui on the same model prints the running session's URL instead of opening another.

Option Description
--port <port> Listen on this port. Default: a free one.
--open Open the page in the default browser. Without one, open the printed URL yourself.
--grace <seconds> Keep running this long after the last client leaves. Default: 30.
--log <file> Append the session's request log to this file.

tx ui stops on Ctrl+C, or once no client has been connected for --grace seconds. It never discards unsaved changes on its own: with changes unsaved, the grace period passes without stopping, and Ctrl+C asks to be pressed again. Before the first client connects, it waits.

tx ui ./model --open
tx ui ./model --port 7411 --grace 300
tx ui ./model --output-format json            # {"data": {"url", "port", "model", "processId", "joined"}}

While tx ui holds a model, the one-shot commands that work on it (add, bpa run, deps, find, format, get, ls, mv, replace, rm, save, set, summary and validate) run in its session instead of on the files, so tx set from an agent or another terminal shows up in the page and can be undone there. They see the session's unsaved edits; their own edits stay unsaved until tx save (or --save), as in tx interactive. A command that would prompt fails and names the flag to pass instead, and --stage is refused, since the session already holds the edits. --recent and the other commands still work on the files.

tx ui ./model &
tx set Sales/Revenue ./model -p FormatString='#,0'   # one undo step in the session
tx save ./model                                      # writes the session

When the model's files change outside the session, the page says so and offers to reload them or keep the session's version, and tx save from any client fails with TOMIX_SESSION_STALE until one of the two is chosen (tx save --force keeps the session's version), as in tx interactive.

The page is a small companion view for now: the model, its unsaved state, who else is connected, an activity feed of every client's changes, and Undo, Redo and Save. The full tomix UI replaces it. In the Claude desktop app, open the URL in the built-in browser pane to keep the page next to the agent.

mcp — give AI agents a session as tools

tx mcp [model] [options]

Serves a model session to an agent harness (Claude Code, Codex, Cursor and others) as Model Context Protocol tools on stdin and stdout. Each tool calls the session protocol method of the same name (object_set calls object.set), so an agent's edit works as an edit in tx interactive does: it applies to the session at once, becomes one undo step, and stays unsaved until session_save.

When tx ui already holds the model, tx mcp joins that session, so you watch the agent's edits in the page and undo or save them there. Otherwise tx mcp holds a session of its own, and discards what is unsaved when the harness stops it. Start tx ui first to keep the agent's work open after the agent is done. Without a model argument, the agent opens one with session_open. Opening a model that tx ui holds joins that session too.

Option Description
--read-only List only the tools that change nothing: no edits, undo or save.
--log <file> Append the server's log to this file instead of writing it to stderr.

Add it to Claude Code, for every model or for one:

claude mcp add tomix -- tx mcp
claude mcp add tomix-sales -- tx mcp ./model --read-only

Other harnesses take the same command in their MCP configuration:

{ "mcpServers": { "tomix": { "command": "tx", "args": ["mcp"] } } }
Tool What it does Writes
session_open Open a model, or join the tx ui session that holds it no
session_status, session_history The session's state, and the changes undo and redo can step through no
model_summary, model_tree Counts of the model's objects; its objects one level at a time no
object_get, object_find, deps_get As tx get, tx find and tx deps no
dax_check, bpa_run As tx validate and tx bpa run no
object_add, object_set, object_move, object_remove, model_replace As tx add, tx set, tx mv, tx rm and tx replace yes
dax_format, bpa_fix As tx format and tx bpa run --fix yes
transaction_begin, transaction_commit, transaction_rollback Group edits into one labelled undo step yes
session_undo, session_redo, session_save Undo, redo, and write the session to the model's files or server yes

A tool's arguments are the options of its command in camel case (caseSensitive for --case-sensitive), less those that save, stage or write files. tools/list describes each one. Every tool carries readOnlyHint and destructiveHint, so the harness knows which ones to ask you about before they run. A failure comes back as the tool's result, with the TOMIX_* code and the hint the CLI would print, so the agent can correct itself (error codes).

The server tells the agent how to work in the session: read with the tools, which see unsaved edits; group related edits in a transaction; never edit the model's files directly while the session is open; save a TMDL folder or .bim file when the task is done and checks pass, since git and undo can take it back, but ask you before saving to a server or Power BI Desktop; and on TOMIX_SESSION_STALE, ask you instead of choosing.

Refresh policies

Policies are table child objects, inspected and edited with get, set, and rm:

tx get 'Sales/RefreshPolicy' --model ./model.tmdl
tx set 'Sales/RefreshPolicy' -p 'IncrementalPeriods=7' --model ./model.tmdl --save
tx rm 'Sales/RefreshPolicy' --model ./model.tmdl --save

-p is an alias for --set; repeat either option to apply multiple assignments as one edit.

Property Description
Mode Import (default) or Hybrid; hybrid requires a compatible model.
RollingWindowPeriods, RollingWindowGranularity Archive window length and unit: day, month, quarter, year.
IncrementalPeriods, IncrementalGranularity Incremental refresh window length and unit.
IncrementalPeriodsOffset Offset of the refresh window.
SourceExpression M query referencing RangeStart and RangeEnd.
PollingExpression Optional change-detection M query; an empty value clears it.

To create a policy, supply both window lengths and granularities and its source expression together. Missing RangeStart/RangeEnd DateTime parameters are created automatically. For example, read the source query from source.m:

cat source.m | tx set 'Sales/RefreshPolicy' --model ./model.tmdl \
  -p 'RollingWindowPeriods=10' -p 'RollingWindowGranularity=Year' \
  -p 'IncrementalPeriods=3' -p 'IncrementalGranularity=Day' \
  -p 'SourceExpression=-' --save

In PowerShell, use Get-Content -Raw source.m to feed the pipeline. Policy inspection includes validation findings and generated partition names. --force permits saving a policy with validation errors but cannot bypass TOM compatibility requirements. Save, save-to, stage, revert, and no-sync follow the standard mutation lifecycle. Removing a policy leaves its generated partitions in place and reports them; --if-exists permits a missing policy.

Migration from incremental-refresh

The incremental-refresh command has been removed.

Previous workflow Replacement
incremental-refresh show Sales get Sales/RefreshPolicy
incremental-refresh set Sales ... set Sales/RefreshPolicy -p Property=Value ...
incremental-refresh rm Sales rm Sales/RefreshPolicy
Apply policy and load data refresh --table Sales --apply-refresh-policy true
Apply without loading data (apply --no-refresh) refresh --table Sales --policy-only

Refresh operates on the policy already saved on the deployed model. Save or deploy local policy edits first. See refresh for remote targeting, previews, and confirmation. Tomix does not require an --execute flag; use --policy-only for empty-partition bootstrap.