Writing data from a dashboard app
A Dashboard app normally only reads. A data source can also be opened for writing, so the dashboard can save what the user changes — planning figures, overrides, comments, a lock on a finished period.
Writing happens at two levels, and they answer different questions:
| Level | What it changes | Use it for |
|---|---|---|
| One row | A single physical row, addressed by the key the database gives it | An editable list, a comment, a correction |
| A whole selection | Every row a filter selects, in one call | A budget grid cell — a dimension node crossed with a period is a set of rows, not a row |
A budget cell has no key to put in an address, so cell edits use the selection level. One call, one statement, however many rows the selection covers — the dashboard never loops over rows.
Before you start
Section titled “Before you start”Everything here is set by an administrator on the data source, and none of it can be turned on from the dashboard. See Data sources.
| Needed | Why |
|---|---|
| The data source published as its own feed | Only a data source feed can be written. A report’s Data feed is a projection of the data and is always read-only |
| A write level on the data source | Says which actions are open at all: update, add, delete |
| A key in the database | A primary key, or a unique index where no column may be empty. Needed to address one row; a selection change does not need one |
| A write filter, if rows should be lockable | The guard: rows it hides stay readable but cannot be written |
| A row filter, if users see different rows | Decides who may read and write which rows, per signed-in user |
| Permissions on the user’s role | One per action — see Permissions below |
The address to write to is the data source’s Data feed address, shown on the data source page. Write to it as the signed-in user, from the dashboard’s own page, and send JSON. There are no tokens or passwords in the dashboard file.
Changing one row
Section titled “Changing one row”| Request | On success | Needs |
|---|---|---|
PATCH <feed>(<key>) | 204, no body | Write level Update existing rows |
POST <feed> | 201, plus the row as stored | Write level Update and add rows |
DELETE <feed>(<key>) | 204, no body | Write level Update, add and delete rows |
The key goes in the address the way the feed reads it — (42) for a single-column key,
(fyear=2026,dept='SLS') when the key has several columns, and then every part must be there.
- A
PATCHchanges only the columns you send. - A
POSTmust carry every key column: nothing invents a key, and without one the row has no address afterwards. The answer is the row as the database actually stored it, defaults and triggers included. - A new row must land inside the filters. It is read back through them in the same transaction,
and a row the caller could not then read again is rolled back with
400— so aPOSTcannot create a row into somebody else’s selection, or into a locked period. - A key that is already taken answers
409, when the row sitting there is one the caller may read. A clash with a row the filters hide answers the same plain400as any other rejected value: saying “that key is taken” would confirm a row outside the caller’s own rows. - Both filters apply to every write. A row the row filter or the write filter hides answers
404, exactly like a row that is not there — the answer deliberately does not say which of the two it was. - On a
PATCH, columns either filter names are refused as write targets (400). A filter protects its own columns, so a lock cannot be picked one row at a time, and a row cannot be handed to somebody else’s selection. On aPOSTthey may be set once — there is no row to move yet, and the new row is read back through both filters before it is kept.
Changing a whole selection
Section titled “Changing a whole selection”Adjust changes every row a filter selects:
POST <feed>/Deem.Adjust{ "set": { "budget_amount": { "mul": 1.05, "round": 2 } }, "filter": "budget_year eq 2027 and company eq 'NO' and time_month eq 3", "groupby": ["time_month"], "preview": true}set names one operation per column:
| Need | Operation |
|---|---|
| Change by percent | {"mul": 1.05}, {"mul": 0.90} |
| Add or subtract a fixed amount per row | {"add": 500}, {"add": -500} |
| Set every row to the same value | {"set": 1000}, {"set": 0}, {"set": null} to clear |
| Revert to a reference column | {"copy": "previous_budget_amount"} |
| New value from another column | {"mul": 1.05, "of": "previous_budget_amount"} |
| Cost from a margin | on budget_cost: {"mul": 0.70, "of": "budget_amount"} |
mul and add read the column they write unless of names another one. round (0–10 decimals)
rounds per row; leave it out and the column’s own precision decides. It has nothing to round on
a plain set, and is refused there.
One operation per column. Refused with 400: an unknown column, a key column, a calculated column,
mul or add on a column that is not a number, two operations on the same column, and an empty
set.
filter uses the same syntax as a read of the feed and can only narrow: the data source’s own
row filter is added on top, so a selection never reaches past what the user may read. No filter
means the whole data source.
The answer says what happened, not which rows:
{ "changed": 107, "held": 12 }held is the rows in the selection the write filter kept out. They were selected, and deliberately
not written.
One call applies one expression to the whole selection. A different factor per month or per node is one call per factor.
Previewing before saving
Section titled “Previewing before saving”Send the same body with "preview": true and nothing is written. The change is run as a sum
instead, grouped by groupby the way the grid reads:
{ "preview": true, "groupby": ["time_month"], "groups": [ { "time_month": 3, "rows": 107, "current": { "budget_amount": 7241288.00 }, "total": { "budget_amount": 7603352.00 }, "held": { "rows": 12, "current": { "budget_amount": 410000.00 } } } ] }rows,currentandtotalare what the change would hit — the amount now, and after.heldis what the write filter keeps out, unchanged, so the grid can show both halves per period instead of one number for the lot.- The new displayed total is
totalplusheld.current. - Leave out
groupbyfor one group covering the whole selection.
The totals are calculated from the very same expression the save would store, so rounding cannot drift between the preview and the result.
Run the same body again with "preview": false to save. A preview commits nothing and reserves
nothing.
Locking and unlocking
Section titled “Locking and unlocking”A column only the write filter names is the lock, so changing that column is a lock or an
unlock. A column the row filter names is never one: it decides whose row this is, so writing it
would hand the row to another user. It is refused with 400 here just as on a PATCH, and no
permission opens it — when a column is named by both filters, the row filter wins and it stays
refused.
Deem.SetLock is the same call as Adjust under a clearer name:
POST <feed>/Deem.SetLock{ "set": { "budget_locked": { "set": 1 } }, "filter": "budget_year eq 2027 and time_month eq 1 and budget_locked eq 0" }Unlocking is the same call with 0, selecting budget_locked eq 1.
Four rules apply to that column:
- A plain value only.
mul,addandcopyleave the direction unanswerable. - On its own. One lock column and nothing else in the same call.
- One value to start from. Every row selected must already hold the same lock value; a mixed
selection answers
400and writes nothing. This is checked on the rows, not on the filter text — so a filter spanning two values is refused even though it names the column. It is what keeps a hard lock from being lifted by a call aimed at a soft one. - Its own permission, decided by direction. Insight counts the writable rows before and after the change: more of them afterwards is an unlock, anything else is a lock. Without the matching permission nothing is written. A preview of a lock change needs one of the two as well — only the save itself can tell which way the change goes, so that is where the direction is checked.
Insight knows nothing about budgets here. The same mechanism expresses a record lock or a current-version marker — whatever column the write filter names.
Permissions
Section titled “Permissions”Access is enforced on the server for every call. Hiding a button in the dashboard is a display
choice, not security: a shown button still gets 403 when the permission is missing.
One permission per action, granted on a role and scoped to the data source
(<connection key>/<data source key>):
| Action | Permission |
|---|---|
PATCH one row | ds-odata:update:<connection>/<source> |
POST a row | ds-odata:create:<connection>/<source> |
DELETE a row | ds-odata:delete:<connection>/<source> |
Adjust and SetLock, preview included | ds-odata:bulk:<connection>/<source> |
| Locking | ds-odata:lock:<connection>/<source> |
| Unlocking | ds-odata:unlock:<connection>/<source> |
None of them implies another — being allowed to correct one row is not being allowed to rewrite two
million. unlock covers locking as well. Reading needs api-odata:read and access to the data
source, as before.
Which rows a user may edit is not a permission. That is the data source’s row filter, resolved per signed-in user, and it applies to reads, writes, selections and previews alike. “A department manager edits only their own department” is one filter on the data source, not a role per department.
When a call fails
Section titled “When a call fails”| Status | Meaning | What the dashboard should do |
|---|---|---|
400 | Bad body, unknown column, a lock rule broken, a new row landing outside the filters, or no usable key on the table | Show the message and fix the call |
401 | Not signed in | Show a “not signed in” message |
403 | The permission for that action is missing | Say which action is missing; do not retry |
404 | No such row, or a filter hides it | Re-read; the row may be locked or outside the user’s rows |
405 | The write level does not open that action, or the address is a report feed | A configuration matter — tell the administrator |
409 | A key that is already taken, a selection larger than the data source’s row limit, or a one-row change matching more than one row | Nothing was written — narrow the selection, or use the row that is already there |
Each call is its own transaction. “Save and then lock” is two calls and can half-succeed, so report each result on its own. There is no way to send several changes as one unit, and no first-writer-wins check: if two people change the same row at the same time, the last write stands.