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:

text
<slug>[--tag--tag]__<scope>__<timestamp>__<uid>
PartRule
slugslugify(name) — lowercased, every run of non-alphanumerics becomes _.
--tagOptional tags, each slugified, up to 5; joined with --.
scopeg for a global dashboard, otherwise the actor/user token.
timestampUTC YYYYMMDDTHHMMSSZ.
uid8 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:

text
<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.json

The prefix is configurable with STRATABI_DASHBOARD_PREFIX. Two conventions worth knowing:

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.

json
{
  "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.

FieldMeaning
system_bucketBucket to write the artifact into.
result_s3_keyExact key to write the artifact to (see pattern below).
result_kinddataframe, json, or artifact.
result_formatparquet, csv, json, md, html, …
block_typeThe block that will render the result.
runtime_tile_keyStatus table hash key (session-scoped).
run_idStatus table range key for this run.
dashboard_tile_keySession-independent tile key.
dashboard_key / dashboard_key_hashThe owning dashboard and its hash.
tile_id, runtime_session_id, deployment_idIdentity of this execution.
input_hash, exec_typeInput fingerprint and execution type.
status, message, tracebackCurrent state (starts REQUESTED).
created_at, updated_at, ttlTimestamps and DynamoDB TTL.

The result key is deterministic:

text
<result_prefix>/dash=<dashboard_key_hash>/session=<runtime_session_id>/tile=<tile_id>/run=<run_id>/<result_name>

Status lifecycle

Status values are uppercase:

text
REQUESTED -> QUEUED -> PENDING -> RUNNING -> SUCCEEDED
                                          -> FAILED
                                          -> CANCELLED

The 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:

python
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:

json
{ "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:

json
{
  "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.