Naming & payload conventions
Reference material for module developers who need to reach past the basics — how StrataBI names and stores dashboards, and the exact payload a module Lambda receives and reports against. You don't need this to build a standard module; it's here for when you're doing something fancy.
Dashboard naming
When the builder saves a dashboard it derives a dashboard identifier (the file stem) from the display name, scope, time, and a random suffix:
<slug>[--tag--tag]__<scope>__<timestamp>__<uid>| Part | Rule |
|---|---|
slug | slugify(name) — lowercased, every run of non-alphanumerics becomes _. |
--tag | Optional tags, each slugified, up to 5; joined with --. |
scope | g for a global dashboard, otherwise the actor/user token. |
timestamp | UTC YYYYMMDDTHHMMSSZ. |
uid | 8 random hex characters (collision guard). |
Example: sales_by_region--finance__20260505T120000Z__alex__a1b2c3d4
The identifier is stored as a JSON object in S3, partitioned by save-month and scope:
<dashboard_prefix>/<YYYY_MM>/<scope>/<identifier>.json
# default prefix: analyst/dashboards
# scope: global | users/<actor>
analyst/dashboards/2026_05/users/alex/sales_by_region--finance__20260505T120000Z__alex__a1b2c3d4.jsonThe prefix is configurable with STRATABI_DASHBOARD_PREFIX. Two conventions worth knowing:
defaultis the reserved id of the global default dashboard.- A
__g__segment marks a dashboard as global; otherwise it is owned by the
actor named in the scope and is only listed for that user.
A dashboard's human label comes from the JSON name (or label) field when present, falling back to the de-slugified identifier.
Async payload
For an async tile (exec.async: true, or a lambda tile), core invokes the runner — and for module tiles, your Lambda — with a single event. The authoritative part is status_context; the rest is convenience context.
{
"status_context": { "...": "see below" },
"inputs": { "region": "north" },
"tile": { "id": "sales", "exec": { "...": "..." }, "block": { "...": "..." } },
"tile_id": "sales",
"run_id": "01J...",
"dashboard_key": "analyst/dashboards/2026_05/users/alex/....json",
"runtime_session_id": "sess-...",
"runtime_tile_key": "<deployment>#<dash-hash>#<session>#<tile>",
"dashboard_tile_key": "<deployment>#<dash-hash>#<tile>",
"status_writer_lambda_arn": "arn:aws:lambda:...:status-writer",
"module": { "module_id": "sales_demo", "version": "0.1.0",
"lambda_index": 0, "lambda_name": "..." },
"deployment_id": "<deployment>"
}status_context
This is the descriptor your Lambda must honor. It carries where to write, what to write, and the keys to report status against.
| Field | Meaning |
|---|---|
system_bucket | Bucket to write the artifact into. |
result_s3_key | Exact key to write the artifact to (see pattern below). |
result_kind | dataframe, json, or artifact. |
result_format | parquet, csv, json, md, html, … |
block_type | The block that will render the result. |
runtime_tile_key | Status table hash key (session-scoped). |
run_id | Status table range key for this run. |
dashboard_tile_key | Session-independent tile key. |
dashboard_key / dashboard_key_hash | The owning dashboard and its hash. |
tile_id, runtime_session_id, deployment_id | Identity of this execution. |
input_hash, exec_type | Input fingerprint and execution type. |
status, message, traceback | Current state (starts REQUESTED). |
created_at, updated_at, ttl | Timestamps and DynamoDB TTL. |
The result key is deterministic:
<result_prefix>/dash=<dashboard_key_hash>/session=<runtime_session_id>/tile=<tile_id>/run=<run_id>/<result_name>Status lifecycle
Status values are uppercase:
REQUESTED -> QUEUED -> PENDING -> RUNNING -> SUCCEEDED
-> FAILED
-> CANCELLEDThe app treats anything other than SUCCEEDED on a finished run as failed — so a module that finishes must report exactly SUCCEEDED, and must report FAILED (not a custom string) on error.
Reporting status
Report by invoking the status-writer Lambda named in status_writer_lambda_arn with the status_context fields spread in, plus status and any extras:
payload = { **status_context, "status": "SUCCEEDED",
"rows": len(df), "columns": list(df.columns) }
lambda_client.invoke(FunctionName=arn, InvocationType="Event",
Payload=json.dumps(payload, default=str).encode())The writer keys the record by (runtime_tile_key, run_id), which is how the app finds your result. If status_writer_lambda_arn is absent, write the item to the status table directly (the starter templates show both paths). Floats must be converted to Decimal for DynamoDB.
See Async & artifacts and Build a module for the end-to-end flow.
Multiline fields: a string or an array of lines
A few fields hold a block of text — Athena query.sql, the markdown block's content, and the raw_html block's html. Each accepts either a single string or an array of strings. When you supply an array, the runtime joins the elements with newlines; a plain string is used as-is. This is purely an authoring convenience — it keeps embedded SQL readable in the JSON and produces clean git diffs, with no change to how the value is executed or rendered.
A single string still works:
{ "query": { "sql": "SELECT region, SUM(revenue) AS revenue FROM sales GROUP BY region" } }…and the array-of-lines form is equivalent, just easier to read and review:
{
"query": {
"sql": [
"SELECT region, SUM(revenue) AS revenue",
"FROM sales",
"WHERE month = :month",
"GROUP BY region"
]
}
}The same applies to markdown.content and raw_html.html. The published dashboard schema accepts both forms (string or array of strings), so hand-authored and AI-generated dashboards validate either way. Source references ({"?source": "…"}) are unaffected — they resolve to text before this normalization.
Shaleio