Authorize

#Canonical Example — the Tasks domain

This is the full worked example. It threads the concept guides together on one tiny, generic domain so you can see how a data model, a workflow, an app spec, and cross-tenant sharing fit into a single build. Each step links to the guide that covers it in depth; the snippets here carry only the concrete Tasks requests unique to the example.

Everything is a live API call against https://api.helm.bridge-labs.com, authenticated with a Personal Access Token over HTTP Basic auth (base64(client_id:client_secret)).

The domain is intentionally minimal:

Project ──< Task >── Assignee

#1. Data model

Three entity types, each with a JSON attribute schema and a stable identifier key:

  • Project — { key, name, status }, identified by key.
  • Task — { key, title, state, priority }, identified by key, with state and priority indexed as filterable and sortable.
  • Assignee — { email, name }, identified by email.

Two relationship rules connect them: ProjectToTask (one-to-many) and TaskToAssignee (many-to-one). The flow is always schema → entity type → relationship rule → entities → links — see Entities & Data Model for the full mechanics.

Register the Task attribute schema first:

POST/schema/schemasRegister the Task schemaAPI docs ↗Try it

Only key is required. A merge upsert validates the attributes it sends, and the reusable job in step 2 sends just key and state — a required title would fail that job.

Then bind it to an entity type, declaring the key attribute as its stable identifier key (what a bulk upsert matches an existing row on) and which attributes are indexed:

POST/entity/entity-typesCreate the Task entity typeAPI docs ↗Try it

Repeat for Project (schema project_attributes, identifier key) and Assignee (schema assignee_attributes, identifier email), then create the two relationship rules. A rule names its types directly; max_to_cardinality: 1 lets each Task belong to at most one Project:

POST/entity/relationship-rulesCreate the ProjectToTask ruleAPI docs ↗Try it

TaskToAssignee is the same shape from Task to Assignee with "max_from_cardinality": 1, so each task has at most one assignee.

#2. Workflow — an inline action and a referenced job

The workflow onboard-project runs two job steps:

  1. Inline job — an upsertEntity action that creates a "Kickoff" Task for the new project, authored directly on the workflow step.
  2. Referenced job — invokes the reusable complete-task job (defined once under /definition/jobs), which marks that task done.

Inline keeps step-local logic next to the workflow; a referenced job is reusable across workflows, and which job the reference runs is a binding each environment can decide for itself. Smart values thread the trigger payload through both steps. See Workflows and Smart Values — a step's output binds as outputs.<stepId>.

First the reusable job:

POST/definition/jobsDefine the complete-task jobAPI docs ↗Try it

Then the workflow that creates the kickoff task inline and calls that job:

POST/definition/workflowsDefine the onboard-project workflowAPI docs ↗Try it

#3. Activation & monitoring

A definition is draft until it's activated for your tenant — activation merges env, reconciles the workflow's triggers, and can bind an access policy for its runs (policy_id, or an inline policy). It's idempotent. The response lists each trigger with its webhook_url. See Triggers.

POST/workflows/{id}/activateActivate onboard-projectAPI docs ↗Try it

Start a run by naming the workflow and its trigger; input becomes context.input for the jobs. It answers 202 with the new run (its id is the run id). A webhook caller would instead POST to the trigger's webhook_url.

POST/workflows/wrRun onboard-projectAPI docs ↗Try it

Each invocation produces a run you can inspect. List runs filtered by workflow:

GET/workflows/runs/workflowsList runs for a workflowAPI docs ↗Try it

Pass workflow_id as a query parameter, then drill into a single run for its per-job and per-action detail via GET /workflows/runs/workflows/{id}: its job_runs carry each job's status, error and an action_log. The run ends success — the ACME-kickoff task then reads "state": "done" — or error, with the failing action and its reason in error.

#4. App spec — a dashboard with live widgets

The app spec (the spec_document body on the wire) defines one dashboard, tasks, with a board page composed of three widgets:

  • a headline that renders from config alone,
  • a data grid of tasks, bound by name to a $query entry in the page's dataSources map. On an entity source a column's field is the bare attribute name (key, not attributes.key), and the grid pages, sorts and filters on the server — see Data Grids,
  • an actions widget exposing an "Add task" command: an inline actions step running upsertEntity (the same action catalog a workflow job uses), then a notify toast and a refresh.

The spec also carries a sedna provider, whose identity block reads the caller from the render request, and a member policy that every user of the app acts under (defaultPolicy). Without a policy granting read on Task, the grid renders empty and the render carries an ERROR notification Data for "tasks" couldn't be loaded: Entity type "Task" not found.

POST/definition/specsCreate the Tasks app specAPI docs ↗Try it

The full widget set — light widgets that render from config and heavy widgets that need authored sample data — is documented, with live previews, in the Widget Catalogue. Here is the headline rendered live:

Loading live widget preview…
{
  "widgetName": "headline",
  "config": {
    "text": "Tasks",
    "headerLevel": 1
  }
}

See App spec structure for every spec_document part.

#5. App activation (config, integration, token)

Activating the spec for a tenant creates the activation chain (see Apps (Bridge)). Each call below uses the spec's id from step 4 as {specId}.

  1. Activate the spec. This provisions the spec's member policy into your tenant:

    PUT/apps/{specId}/configActivate the Tasks appAPI docs ↗Try it
  2. Register the provider organisation the app renders for — provider is the spec's provider key, and external_id is what the spec's company_external_id extractor reads (company.identifier):

    POST/access/external-companiesRegister the provider organisationAPI docs ↗Try it
  3. Wire an integration to that organisation, using the id the previous call returned (GET /access/external-companies lists it again):

    POST/apps/{specId}/integrationsCreate the integrationAPI docs ↗Try it
  4. Mint the run token for the integration. The response carries raw_token (beside data) exactly once — capture it:

    PUT/apps/{specId}/integrations/{integrationId}/tokenMint the app tokenAPI docs ↗Try it

Render the app as the provider would, with the app token and the payload the spec's identity block reads:

curl -X POST https://api.helm.bridge-labs.com/apps/run/<integration_id> \
  -H "Authorization: Bearer helm_at_<your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "user": { "id": "u-1", "emailAddress": "jane@acme.example", "firstName": "Jane" },
    "company": { "identifier": "acme" }
  }'

The 200 carries surfaces, state and referenceData. The grid's rows — the tasks the workflow created, highest priority first — come back under referenceData, keyed by the grid's element id (:p/board/task_table/data_grid:). To press "Add task", send the same payload plus the state from that response and "action": ":p/board/task_actions/0:"; the answer is a full re-render with an INFO notification Task added and the new task among the grid's rows. See Apps → Interactions.

#6. User provisioning

Because the provider is configured to auto-create users (autoCreateUsers), the first render for a new provider user provisions the tenant user from the spec's identity block — see Apps → Providers & identity. GET /access/external-users lists the provisioned users with their auth_user_id. To put one user under a specific spec policy, assign it by key (PUT /apps/{specId}/users/{authUserId}/policy with { "policy": "member" }). See Provisioning.

#7. Shares & grants

To let a principal read Tasks within your tenant, create an intra-tenant grant (tier-2). grantee_id is a policy's id (POST /access/policies creates one), and for entity rows resource_id is the Task entity type's id:

POST/access/grantsGrant read on Task rowsAPI docs ↗Try it

To let another tenant build on this — activate the shared spec, or store their own Tasks of the shared type — the owner lists the resources in a manifest and offers it; the other tenant accepts, which creates read-only shares (tier-1), and activates the spec in its own tenant. The manifest must list the Task entity type as well as the tasks_app spec, because the spec's policy names it. Cross-tenant data reads need both a share and a grant. See Share with Another Tenant, Accept & Activate, and Shares & Grants.

#8. The same read over GraphQL and in a workflow

The grid's data comes from an engine-native $query source over Task — the same filter/sort/include vocabulary as POST /entity/query (see Entities & Data Model → Read & query). The same data is also reachable over GraphQL (POST /entity/graphql; root fields are PascalCase, operators are underscore-prefixed, and filterable attributes nest under attributes:). Prototype the read in the playground at /graphql/:

POST/entity/graphqlOpen tasks, highest priority firstAPI docs ↗Try it

orderBy accepts only attributes indexed with sortable: true, which is why step 1 marks priority sortable. The same read as a workflow step is a queryEntity action — the root field becomes typeName, each where condition a filter on ["attributes", "<name>"], and orderBy the sort:

{
  "stepId": "open-tasks",
  "name": "queryEntity",
  "config": {
    "typeName": "Task",
    "filter": {
      "operator": "eq",
      "path": ["attributes", "state"],
      "value": "open"
    },
    "sort": [{ "path": ["attributes", "priority"], "direction": "desc" }],
    "limit": 10
  }
}

Both return the same tasks in the same order; in the workflow the rows land in outputs.open-tasks.data. See Querying & GraphQL.