Skip to content

Private Preview

Business Workflows and Apps

7 min read · Updated 12 Nov 2026

You can author Business Workflows in the semantic layer, including writable tables, stored procedures and virtual procedures. An App runs one by name with app.runDataAction().

runDataAction() works in astrato dev and in a published App when all of these hold:

  • Business Workflows are enabled for the tenant, and the workflow is registered and published in the semantic layer.
  • Signed-in viewers have access to the semantic layer, and data-action access is turned on for your Astrato environment. This is not a setting on the layer or the publication: Astrato turns it on, and a viewer’s sign-in then carries the data:action permission. Without it, a run is refused with forbidden. See Run Business Workflows from a Data App.
  • The data connection has an explicit, non-legacy Deployment Region, the same one a Data App needs to load data. See Choose a Deployment Region.
  • Your App uses @astrato-io/app-runtime 0.5.0 or later, the first version with runDataAction().
  • For astrato dev, use @astrato-io/cli 0.1.3 or later. Its login includes the data:action scope, so run astrato login again after upgrading; a login saved by an earlier version lacks it.

Public publications cannot run workflows: an anonymous viewer is refused. Preview connections in the Astrato editor are read-only.

Workflow discovery and execution-status lookup are not supported by the App runtime or CLI.

To register a workflow first, see Business Workflows. For the whole route from a mapped table to a saved row, follow Let users edit data and save it back.

The App names a registered workflow and passes one object of named arguments per run. It cannot name a data source, schema or SQL.

const result = await app.runDataAction({
name: 'Resolve comment',
arguments: [{ commentId: 'c-102', resolution: 'Fixed' }],
});

Omit arguments to run a workflow without parameters once. A single call takes up to 1,000 argument objects.

For a writable table, each key field and writable field is an argument named after the field in the semantic layer, not the warehouse column. An Update rows workflow named Update order status, with key field order_id and writable field status, takes:

await app.runDataAction({
name: 'Update order status',
arguments: [{ order_id: 1042, status: 'Shipped' }],
});

Send every key field, including each one of a composite key, and every writable field. A field whose value the workflow takes from a user attribute is filled by Astrato and is not sent. Anything else is refused with unknown_parameter. A workflow allows one operation, so a screen that edits and adds rows runs an Update rows workflow and a separate Insert rows workflow.

The workflow that runs is the published version of the semantic layer. A saved draft or an unpublished edit is never run, so publish the layer after changing a workflow. A published App is pinned to the layer revision it was published against. If the layer has been published again since, the call is refused with app_unavailable; republish the App.

Astrato runs the workflow as the signed-in viewer, so the viewer’s own permissions on the layer and the data source apply. The App has no confirmation step of its own from Astrato: ask the viewer in your interface before you call runDataAction().

When a step belongs to another role, for example a checker approving a maker’s change, say next to the control which role can take it. A disabled button with no reason reads as a fault. Keep the role check in the workflow itself, so it protects the data when the interface is wrong, and take the viewer’s identity from the signed-in user rather than from an App argument; see Restrict a write to the signed-in user. A refusal raised by the workflow’s own SQL arrives as a failed outcome, not as data_action_forbidden; see Return an error message.

runDataAction() resolves with one outcome per argument object:

{
"dataAction": "Resolve comment",
"invocations": 1,
"outcomes": [{ "inputIndexes": [0], "status": "ok", "effects": "committed" }]
}

status is ok, failed, notExecuted or unknown. effects is none, committed, rolledBack or unknown. A large result keeps each outcome’s status and effects and drops its rows, and resultTruncated is true.

A failed outcome carries an error with a code and, when the engine names it, the statement that failed. When the workflow’s own SQL wrote the message, error.authored is true and error.message is that message, with the code DATA_ACTION_AUTHORED_ERROR. Show that message as written. A failure the workflow reports with its own application code can also carry a message while authored is absent: it is the workflow’s text too, and an App may show it, while Astrato’s own screens distinguish authored messages from engine guidance. Recognized engine failures carry authored: false, a safe message, and, when available, class and retryable. These messages explain missing permissions or objects, procedure signature mismatches, invalid error tags and reserved markers in inputs. Unclassified failures have no displayable message. Raw warehouse diagnostics, SQL and rejected input values are not guidance.

DATA_ACTION_AUTHORED_ERROR_INVALID carries safe guidance explaining why an authored tag was refused, without a failure class. It is engine guidance, not the rejected tag content.

{
"inputIndexes": [0],
"status": "failed",
"effects": "rolledBack",
"error": { "code": "DATA_ACTION_AUTHORED_ERROR", "message": "Budget locked.", "authored": true, "statement": "check" }
}

Render error.message as plain text, never HTML. Existing Apps filtering for authored: true must update their error handling and be republished to show engine guidance. retryable does not authorize an automatic retry.

Astrato shows the viewer nothing when a workflow runs: the App’s own interface reports the outcome. Show a confirmation when every outcome is ok, show error.message when an outcome is failed and has one, offer to rerun only the arguments at the failed outcome’s inputIndexes when its error.retryable is true, show a generic message when it has no message, show the error message when the call rejects, and run affected queries again after a committed change, because existing results do not refresh by themselves. If any outcome has unknown effects, check the data before running the workflow again.

The engine classifies common warehouse errors and returns a fixed message, never the warehouse’s own text. The message identifies the permission, object or parameter when the engine can establish it safely; raw SQL and rejected input values remain private.

class error.code retryable
procedureSignatureMismatch DATA_ACTION_PROCEDURE_SIGNATURE_MISMATCH false
invalidInput DATA_ACTION_RESERVED_ERROR_MARKER false
invalidInput DATA_ACTION_INVALID_INPUT false
objectNotFound DATA_ACTION_OBJECT_NOT_FOUND false
constraintViolated DATA_ACTION_CONSTRAINT_VIOLATED false
permissionDenied DATA_ACTION_PERMISSION_DENIED false
timeoutOrLimit DATA_ACTION_TIMEOUT_OR_LIMIT true if effects is none or rolledBack
concurrencyConflict DATA_ACTION_CONCURRENCY_CONFLICT true if effects is none or rolledBack
connectionUnavailable DATA_ACTION_CONNECTION_UNAVAILABLE true if effects is none or rolledBack
{
"inputIndexes": [0],
"status": "failed",
"effects": "none",
"error": {
"code": "DATA_ACTION_INVALID_INPUT",
"message": "The warehouse rejected an input value. Check its type and allowed range before trying again.",
"authored": false,
"class": "invalidInput",
"retryable": false
}
}

Classification coverage depends on the warehouse and failure. Unrecognised warehouse failures keep the generic EXECUTION_FAILED code. Engine validation refusals, such as invalid authored tags and reserved markers in inputs, are separate from warehouse-error classification. In Astrato’s own screens, an input form in Nash shows this message and adds “Nothing was changed, so it is safe to run it again” only when retryable is true. For a classified failure that is not retryable, the thread summary advises not to run it again unchanged.

A refusal rejects with an AstratoAppRuntimeError whose response.error carries the code and a short message. The workflow’s own message reaches the App, for example OVERRIDE_REQUIRED: a manager must approve. Warehouse diagnostics and SQL do not. To return a message from a SQL statement, see Return an error message from a Business Workflow.

Code Meaning
data_actions_disabled Business Workflows are not enabled for the tenant.
data_action_not_found The published layer has no workflow with that name.
invalid_arguments, unknown_parameter, missing_parameter The arguments do not match the workflow’s declared parameters.
invalid_parameter_value A value cannot be held by its parameter’s type, for example text for an integer or a number for a string. Only a decimal parameter takes decimal text.
data_action_refused The workflow or the data source refused the run.
exit_node_required The data connection has no exit node that can run workflows.
data_action_execution_unavailable The data connection cannot execute workflows.
data_action_connection_unsupported The connection’s driver cannot run workflows. Use a supported connection.
data_action_connection_inaccessible The caller cannot access the connection the layer uses.
forbidden The viewer cannot open the App or its data, or the sign-in lacks data:action.
data_action_forbidden The viewer is not allowed to run it.
data_action_anonymous_refused The viewer is anonymous on a public publication.
data_action_scope_required The astrato dev sign-in lacks data:action. Sign in again.
data_action_outcome_unknown The answer was lost; the write may have run.

Never run a workflow again after data_action_outcome_unknown, after a rejection whose message says the outcome is unknown, or after an outcome with effects: "unknown". Check the data first. Astrato does not retry a run for you.

Existing explicit table-write and stored-procedure APIs are documented separately in Writeback and SQL. They do not execute semantic Business Workflow definitions or enforce their authored settings, and they remain unavailable from a published App.