Build a module
A module is a customer-owned extension: one or more Lambdas, a manifest, and the Terraform that deploys and registers them. Modules add compute — and optionally UI — to StrataBI without touching core. You build them in your own repo and deploy them into your own account.
Two families of module
"Module" covers a few different things, so it helps to split them into two families up front:
- Executable modules (
single_lambda,microservices) ship **compute only,
no UI**. They are functions a dashboard tile calls by lambda_index — think "a backend a tile can invoke."
- App modules (
app,multi_page_app) ship a UI: one or more dashboard
pages bundled with the module. Core renders them as a self-contained micro-app inside the StrataBI runtime, served at the module's own route. The pages are ordinary dashboard JSON, rendered exactly like standalone dashboards; they just live inside the module and can call the module's own lambdas.
module_type in the manifest selects which one you're building. This guide follows a working single_lambda example end to end, then covers the app types.
Structure
An executable module:
my_module/
module.json manifest (conforms to module.schema.json)
src/<function_id>/handler.py the Lambda code
src/<function_id>/requirements.txt dependencies (empty = stdlib + boto3)
terraform/main.tf packages, deploys, and registers the module
terraform/variables.tf
terraform/terraform.tfvars.example
llm_context.txt agent manual for this module (see below)
README.mdAn app module adds a pages/ folder — one dashboard JSON per page — that the runtime serves as a micro-app:
my_app_module/
module.json manifest: module_type app / multi_page_app
pages/
overview.json a dashboard layout (same shape as a standalone dashboard)
detail.json another page
src/<function_id>/handler.py lambdas the pages call (optional but typical)
terraform/...
llm_context.txt
README.mdEach file in pages/ is a normal dashboard JSON (a name plus a layout of tiles). The manifest's pages[] declares page order, labels, ids, and navigation; the pages/ folder is where you keep each page's layout while you author it. See UI modules below.
1. The manifest — module.json
The manifest declares the module. It validates against the module schema.
{
"$schema": "https://shaleio.com/schemas/stratabi/module.schema.json",
"schema_version": "1.0",
"module_id": "sales_demo",
"module_type": "single_lambda",
"label": "Sales Demo",
"description": "Returns a per-category sales summary as a dataframe artifact.",
"version": "0.1.0",
"service_dependencies": [
{ "service_type": "lambda", "name": "sales_demo.sales_summary", "required": true }
],
"lambdas": [
{
"function_id": "sales_summary",
"handler": "handler.handler",
"runtime": "python3.12",
"memory_mb": 256,
"timeout_seconds": 30,
"trigger": "dashboard_event"
}
]
}| Field | Notes |
|---|---|
module_id | Stable id. Must match the S3 discovery prefix (see step 3). |
module_type | single_lambda, app, multi_page_app, or microservices. |
service_dependencies | The AWS services the module needs (required by the schema). |
lambdas[] | One entry per function: function_id, handler, runtime, memory_mb, timeout_seconds, environment, trigger. |
single_lambda modules are executable only — they ship no dashboard UI. UI-bearing module types (app, multi_page_app) carry a pages[] array of dashboard layouts; see the module schema for those fields.Module types
module_type selects the shape of the module. Every type declares lambdas[]; the UI types add pages[]. These requirements come straight from the module schema:
| Type | Lambdas | Pages | What it is |
|---|---|---|---|
single_lambda | exactly 1 | — | One function a dashboard calls by lambda_index 0. |
microservices | 1 or more | — | A bundle of functions, called by index. |
app | 1 or more | exactly 1 | A self-contained single-page dashboard shipped with the module. |
multi_page_app | 1 or more | 1 or more | Multiple pages plus a navigation_type. |
Executable modules — single_lambda, microservices
No UI. Dashboards reference each function by module_id + lambda_index (the order of entries in lambdas[]). microservices is simply single_lambda with more than one function:
{
"module_type": "microservices",
"lambdas": [
{ "function_id": "summary", "handler": "handler.handler", "runtime": "python3.12" },
{ "function_id": "forecast", "handler": "handler.forecast", "runtime": "python3.12" }
]
}UI modules — app, multi_page_app
An app module is a micro-app deployed inside the StrataBI runtime. It bundles one or more dashboard pages with the module; core serves them at the module's own route and renders each page exactly like a standalone dashboard. The difference from a normal dashboard is only where it lives: inside the module, versioned and deployed with it, able to call the module's own lambdas by lambda_index.
Pages live as dashboard JSON in a pages/ folder. Keep one file per page — pages/overview.json, pages/detail.json — each a normal dashboard layout (a name plus a layout of tiles). This is the authoring convention: the layouts are ordinary dashboards you can validate against dashboard.schema.json.
The manifest then declares the page set — id, label, order, and navigation. Each manifest page carries the same page_id / label / layout the schema defines (the layout is the tile array from the matching pages/<page_id>.json):
- An
apphas exactly one page — a single self-contained dashboard
shipped with the module.
- A
multi_page_apphas one or more pages plus anavigation_type
(tabs, pagination, or next_previous) and optional overrides (default_page, show_labels, show_progress, show_first_last, loop).
{
"module_type": "multi_page_app",
"navigation_type": "tabs",
"lambdas": [ { "function_id": "metrics", "handler": "handler.handler", "runtime": "python3.12" } ],
"pages": [
{ "page_id": "overview", "label": "Overview", "layout": [] },
{ "page_id": "detail", "label": "Detail", "layout": [] }
]
}A tile on one of those pages references the module's own function the usual way — "exec": { "type": "lambda", "module_id": "<this module>", "lambda_index": 0 } — so an app module is fully self-contained: its pages and the compute they call ship and deploy together.
single_lambda module — the simplest, fully working type. The app / multi_page_app manifest fields are defined by the module schema; the pages/ folder is the authoring convention for the page layouts. How core lays out navigation between pages is owned by the runtime.2. The function — handler.py
A module Lambda implements the StrataBI async tile contract. Core invokes it with a status_context that tells it exactly where to write and how to report:
def handler(event, context=None):
ctx = event["status_context"]
bucket = ctx["system_bucket"]
key = ctx["result_s3_key"]
result_format = ctx["result_format"] # parquet | csv | json | ...
try:
df = build_dataframe(event.get("inputs") or {})
write_dataframe(bucket, key, df, result_format)
notify_status_writer(event.get("status_writer_lambda_arn"),
{**ctx, "status": "SUCCEEDED",
"rows": len(df), "columns": list(df.columns)})
except Exception as e:
notify_status_writer(event.get("status_writer_lambda_arn"),
{**ctx, "status": "FAILED", "message": str(e)})
raiseThe rules:
- Write your artifact to
s3://{system_bucket}/{result_s3_key}in the
result_format you were given.
- Report
SUCCEEDEDorFAILEDby invoking the status-writer Lambda
(event["status_writer_lambda_arn"]) with the status_context fields spread in. Status must be uppercase.
- For a dataframe tile,
result_kindisdataframeandresult_formatis
typically parquet (also csv/json for lightweight results).
See Modules for the full contract and the two starter templates.
3. Deploy and register — Terraform
The module's Terraform does four things:
- Packages
src/<function_id>/into a Lambda zip and creates the function
(named <name_prefix>-<module_id>-<function_id>).
- Creates a least-privilege execution role: invoke the status-writer Lambda,
write CloudWatch logs, and s3:PutObject only under the runtime result prefix.
- Uploads
module.jsonto S3 for discovery:
s3://<system_bucket>/modules/<module_id>/module.json.
- Writes a registry item to the
stratabi_module_registryDynamoDB table
(hash key module_id) containing lambda_arns, lambda_function_names, module_s3_uri, a copy of the manifest, and timestamps.
Steps 3 and 4 are what make the module discoverable: at runtime core resolves module_id + lambda_index through the registry to a concrete Lambda ARN.
Required Terraform inputs:
| Variable | Description |
|---|---|
aws_region, aws_account_id | Target account. |
stratabi_system_bucket | Existing StrataBI system bucket. |
module_registry_table | Registry table (default stratabi_module_registry). |
module_prefix | S3 discovery prefix (default modules). |
runtime_result_prefix | Where the Lambda may write results (default runtime/results). |
status_writer_lambda_arn | Status-writer Lambda the module invokes. |
name_prefix | Resource name prefix (default stratabi). |
module_id must equal the S3 child prefix: modules/<module_id>/module.json. If they disagree, core won't discover the module.4. llm_context.txt — the module's agent manual
Alongside module.json, ship an llm_context.txt: a short, plain-text manual that tells an agent how to use this specific module from a dashboard — its id, each function's inputs and outputs, and an example tile.
# StrataBI module: sales_demo
- module_id: sales_demo
- version: 0.1.0
- summary: per-category sales summary as a dataframe artifact
## Functions
- lambda_index 0 (sales_summary)
inputs: year (optional integer) — filter to one year
output: dataframe columns category (string), total_value (number)
formats: parquet | csv | json
blocks: table, plotly (bar by category)
## Reference from a tile
{ "id": "sales", "exec": { "type": "lambda", "module_id": "sales_demo",
"lambda_index": 0 }, "block": { "type": "table", "config": {} } }Keep it concise and accurate — it is the contract an agent reads before wiring your module into a dashboard. Upload it to S3 next to module.json (modules/<module_id>/llm_context.txt) so it travels with the module.
5. Reference it from a dashboard
Once deployed and registered, any dashboard can call the module:
{
"id": "sales_by_category",
"exec": { "type": "lambda", "module_id": "sales_demo", "lambda_index": 0 },
"block": { "type": "table", "config": {} }
}Drive a function input (like year) from an input tile — see Inputs & parameters.
6. Declare install variables (optional)
If your module needs configuration at install time, ship an install.schema.json next to the module (or an install_schema block in module.json). StrataCLI reads it and prompts for those values generically — no module-specific code in the CLI:
{
"title": "Sales demo module",
"fields": [
{ "key": "region_filter", "prompt": "Sales region to include",
"tfvar": "region_filter", "type": "string", "default": "all" },
{ "key": "refresh_minutes", "prompt": "Refresh interval (minutes)",
"tfvar": "refresh_minutes", "type": "int", "default": 60 }
]
}Each field supports key, prompt, tfvar, type (string/bool/int/list/enum), default, required, choices, validator, and when. Users then run stratacli init module <id>, which merges the answers into strata.yaml and generates <id>.auto.tfvars.
7. Deploy
stratacli init module <id> # collect + generate the module's tfvars (if it has any)
stratacli install <id> # or run terraform/tofu directly in the module's dirThen add a lambda tile referencing module_id to a dashboard and run it.
Shaleio