Parsware
All articles

Parsware Platform

The Runtime SDK

Everything a custom app does in Parsware goes through one object: who's using it, where it was opened, the data their role can read, saved queries, Custom APIs, object storage and the log. We grew the visitor board with company totals from a saved query, readable visit purposes, and a button that keeps today's list as a file.

The Visitor board in the Shell with company totals 4 Analytical Engines, 2 Bletchley Partners, 2 Orbital Freight above the visitor cards, each card showing a purpose such as Meeting or Delivery, next to the title "The Runtime SDK"

The visitor board reads one table. Reception wants three more things: how many people each company has sent, why each visitor came, and a way to keep today's list for the records. All of it comes through the same object the board already has:

const runtime = useParswareRuntime();

That's the Runtime SDK. The Shell creates it for the environment it opened the app in and for the person using it.

What's in it

runtime.user, runtime.environmentName Who's using the app, and where.
runtime.scopes What the app asked for, narrowed to what the person's session holds.
runtime.data Records, queries, saved queries, Custom APIs, reports, record files, and the definitions of tables, views and forms.
runtime.storage Object storage: buckets and the files in them.
runtime.log The platform's log, with the app and environment attached.

Every call goes out as the person using the app. Their security role decides what each one may read and change, exactly as in a model-driven app.

Totals from a saved query

Visitors by company is a saved query in the Visitors solution: grouped by company, counting visitors (Query platform data for reporting made it). The board runs it by name:

const totals = await runtime.data.runSavedQuery('nw_visitorsbycompany');
// totals.groups.groups → [{ groups: { nw_company: 'Analytical Engines' }, measures: { visitors: 4 } }, …]

The question lives in the solution, not in the app's code, so whoever improves it in the Query designer improves the board too, without a new build.

A choice comes with its label

The cards now ask for Purpose as well:

const result = await runtime.data.runQuery('nw_visitor', {
  select: ['nw_name', 'nw_company', 'new_purpose'],
  order: [{ field: 'nw_company', descending: false }, { field: 'nw_name', descending: false }],
  page: { number: 1, size: 50 },
});
// record.values.new_purpose          → 270150000  (what's stored)
// record.formattedValues?.new_purpose → 'Meeting'  (what a person reads)

A choice column comes back as the option's number, which is what's stored, with its label beside it in formattedValues. A lookup's name travels the same way. So a card shows record.formattedValues?.new_purpose and the board looks nothing up. Grouped results do the same, in formattedGroups.

With both, the board has its totals across the top and a purpose on every card:

The Visitor board in the Shell: totals 4 Analytical Engines, 2 Bletchley Partners and 2 Orbital Freight across the top, a Save today's list button, and eight cards each showing a name, a company and a purpose such as Meeting or Delivery

Keep today's list as a file

A file that belongs to no single record goes into object storage. The board builds a CSV and puts it in a bucket:

async function save() {
  const csv = ['name,company,purpose', ...rows].join('\n') + '\n';
  const day = new Date().toISOString().slice(0, 10);
  await runtime.storage.createBucket('nw-frontdesk');   // or returns the existing one
  const file = await runtime.storage.put('nw-frontdesk', `visitors/${day}.csv`,
    new Blob([csv], { type: 'text/csv' }), `${day}.csv`);
  runtime.log('info', 'Visitor list saved', { rows: rows.length });
  setSaved(`${file.bucket}/${file.key}`);
}

The app asks for storage in its parsware block, "requestedScopes": ["modelapps.read", "storage.write"], and only offers the button when it was granted:

const canSave = runtime.scopes.includes('storage.write');

Version 1.1.0 packed and uploaded over the first one, Save today's list:

The Visitor board after saving: under the heading, Saved to nw-frontdesk/visitors/2026-10-14.csv

And under Storage in the Admin Control Center, the file is there, uploaded by the person who pressed the button:

The Storage screen inside the nw-frontdesk bucket: visitors/2026-10-14.csv, text/csv, uploaded by the signed-in administrator

And the log

runtime.log('info', 'Visitor list saved', { rows: 8 }) goes to the platform's log with the app and the environment attached, where whoever supports the board can find it. The browser's console is invisible to them. Never put record values or personal data in it.

What isn't there

Notifications. The SDK has no way to send one, so an app shouldn't try to fake one with a direct call to the platform's services. There's also nothing to read a token from, and nothing that reaches another environment: the runtime is bound to the one the app was opened in.

What we fixed

  • The app was told it couldn't save files. runtime.scopes is what the app asked for, narrowed to what the person's session holds, and a session held no storage scopes. So the board hid its button, while the same upload worked. Sessions now carry storage.read and storage.write, which people could already use from the Storage screen.
  • The help page for the SDK described an older SDK. It left out queries, saved queries, Custom APIs and storage, said storage wasn't available, and had an example that didn't compile. It's rewritten against what the SDK actually offers, in all seven languages.

Good to know

  • A bucket belongs to your organisation, not to an environment. Put the environment in the key if Development's files and Production's must stay apart.
  • Bucket names are shared with every other Parsware customer, as in S3, so pick one that's clearly yours.
  • A file that belongs to one record goes on the record instead, with runtime.data.uploadRecordFile, and is read and deleted with it.

Try it

Add something your own board needs: a saved query of your own run by name, the label of another choice column, or a file saved to a bucket. Then open the board as somebody whose role reads less, and see what changes.

Next: Build a custom control.