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:

no UI**. They are functions a dashboard tile calls by lambda_index — think "a backend a tile can invoke."

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:

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

An app module adds a pages/ folder — one dashboard JSON per page — that the runtime serves as a micro-app:

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

Each 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.

json
{
  "$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"
    }
  ]
}
FieldNotes
module_idStable id. Must match the S3 discovery prefix (see step 3).
module_typesingle_lambda, app, multi_page_app, or microservices.
service_dependenciesThe AWS services the module needs (required by the schema).
lambdas[]One entry per function: function_id, handler, runtime, memory_mb, timeout_seconds, environment, trigger.
Note
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:

TypeLambdasPagesWhat it is
single_lambdaexactly 1One function a dashboard calls by lambda_index 0.
microservices1 or moreA bundle of functions, called by index.
app1 or moreexactly 1A self-contained single-page dashboard shipped with the module.
multi_page_app1 or more1 or moreMultiple 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:

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

shipped with the module.

(tabs, pagination, or next_previous) and optional overrides (default_page, show_labels, show_progress, show_first_last, loop).

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

Note
The sample in this guide is a 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:

python
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)})
        raise

The rules:

result_format you were given.

(event["status_writer_lambda_arn"]) with the status_context fields spread in. Status must be uppercase.

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:

  1. Packages src/<function_id>/ into a Lambda zip and creates the function

(named <name_prefix>-<module_id>-<function_id>).

  1. Creates a least-privilege execution role: invoke the status-writer Lambda,

write CloudWatch logs, and s3:PutObject only under the runtime result prefix.

  1. Uploads module.json to S3 for discovery:

s3://<system_bucket>/modules/<module_id>/module.json.

  1. Writes a registry item to the stratabi_module_registry DynamoDB 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:

VariableDescription
aws_region, aws_account_idTarget account.
stratabi_system_bucketExisting StrataBI system bucket.
module_registry_tableRegistry table (default stratabi_module_registry).
module_prefixS3 discovery prefix (default modules).
runtime_result_prefixWhere the Lambda may write results (default runtime/results).
status_writer_lambda_arnStatus-writer Lambda the module invokes.
name_prefixResource name prefix (default stratabi).
Caution
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.

text
# 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:

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

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

bash
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 dir

Then add a lambda tile referencing module_id to a dashboard and run it.