Skip to content

Connect a project to LumenFlow Cloud + consume the API

End-to-end walkthrough: create a workspace, issue an API key, run npx lumenflow cloud connect from your project, then call the Sidekick API for actions and the engagement-projection API for read-only cross-project consumption.

Warning

This walkthrough issues an advanced workspace API key — the strongest credential in a workspace, meant for CI jobs and self-hosted automation. It is not part of the consumer setup journey and is not how you pair your own Linux or Windows machine as a Connected Compute node: that flow lives on the workspace Compute page and uses a short-lived, single-use pairing code instead of any long-lived token. Use this page only when you are automating a repository or CI runtime against the Sidekick API.

Info

Prefer a guided experience? The dashboard ships an in-app wizard that walks the same six steps in order: open /dashboard/connect while signed in. The wizard issues the API key, copies the connect command, previews workspace.yaml, and probes /api/v1/health for you.

Two auth surfaces#

LumenFlow Cloud exposes two parallel bearer-token surfaces. Pick the one that matches what your consumer needs to do:

SurfaceUse caseAuth header
Workspace API keyRun Sidekick, dispatch tools, manage agents/jobs/memory/runtime sessionsAuthorization: Bearer <workspace-api-key>
Engagement tokenRead-only cross-project consumption: overview, work, evidence, events, snapshots, embeds, OpenAPI introspectionAuthorization: Bearer <engagement-token> per engagement slug

Workspace API keys are issued per workspace and act on its behalf. Engagement tokens are scoped to a single engagement (a curated view onto a workspace for an external consumer — your customer portal, a partner integration, an embedded dashboard).

Two more kinds of key sit beside these: inference keys for governed model calls, and app keys for a deployed app's server. Runner onboarding grants and org service tokens are the remaining kinds. The prefix, scopes and revoke path of every kind are in Credentials and scopes.

Step 1 — Create a workspace#

Sign in at https://<your-cloud-host>/dashboard and create a workspace. Note the workspace ID (UUID) and your org ID — both show up in the workspace settings page.

Step 2 — Issue a workspace API key#

From the dashboard, hit the Regenerate API key button on the workspace settings page, or call the endpoint directly:

curl -X POST https://<your-cloud-host>/api/v1/workspaces/<workspaceId>/api-key \
  -H "Authorization: Bearer <your-dashboard-jwt>"

The response includes the plaintext key, the connect command, and the full SDK enrollment descriptor:

{
  "workspaceId": "<uuid>",
  "apiKey": "lf_xxxxxxxxxxxxxxxxxxxx",
  "orgId": "<uuid>",
  "connectCommand": "npx lumenflow cloud connect --endpoint https://<cloud-host> --org-id <orgId> --project-id <workspaceId> --token-env LUMENFLOW_CONTROL_PLANE_TOKEN",
  "sdkEnrollment": {
    "contractVersion": 1,
    "runtimeModel": "governed_external_agent",
    "runtimeSource": "connected",
    "bootstrap": {
      "endpoint": "https://<cloud-host>",
      "orgId": "<orgId>",
      "projectId": "<workspaceId>",
      "tokenEnv": "LUMENFLOW_CONTROL_PLANE_TOKEN",
      "workspaceConfig": {
        "endpoint": "https://<cloud-host>",
        "org_id": "<orgId>",
        "project_id": "<workspaceId>",
        "sync_interval": 30,
        "policy_mode": "tighten-only",
        "auth": { "token_env": "LUMENFLOW_CONTROL_PLANE_TOKEN" }
      }
    }
  }
}

The plaintext apiKey is shown once. Store it in a secrets manager immediately. If you lose it, regenerate via the same endpoint — the previous key is invalidated within ~15 seconds on every cloud instance.

Step 3 — Connect the local project#

Put the key in LUMENFLOW_CONTROL_PLANE_TOKEN for the shell you run the command from — a hidden read, never a literal export ...=<secret> in a profile or in history. The command passes only the variable name, so it is the same on Linux, macOS and Windows:

# bash / zsh (Linux, macOS) — paste the key, hidden:
read -rs LUMENFLOW_CONTROL_PLANE_TOKEN && export LUMENFLOW_CONTROL_PLANE_TOKEN
# PowerShell (Windows) — paste the key, hidden:
$env:LUMENFLOW_CONTROL_PLANE_TOKEN = Read-Host -MaskInput 'Workspace key'

Then, in the root of the project you want to govern:

npx lumenflow cloud connect \
  --endpoint https://<your-cloud-host> \
  --org-id <orgId> \
  --project-id <workspaceId> \
  --token-env LUMENFLOW_CONTROL_PLANE_TOKEN

Add --force to re-link a project that already has a control_plane block (for example after regenerating the key). Use the npx lumenflow cloud connect subcommand: the pnpm cloud:connect script that an older CLI version wrote into a consumer repo may not accept these flags.

The CLI stores the key per project in $LUMENFLOW_HOME/secrets/<workspaceId>.env (mode 0600) and writes the cloud bootstrap into your local workspace.yaml:

endpoint: https://<your-cloud-host>
org_id: <orgId>
project_id: <workspaceId>
sync_interval: 30
policy_mode: tighten-only
auth:
  token_env: LUMENFLOW_CONTROL_PLANE_TOKEN
  token_file: $LUMENFLOW_HOME/secrets/<workspaceId>.env

Because the credential lives in a per-project file, several projects on one machine each keep their own key. Never export a workspace key from a shell profile: a global value shadows every other project's file.

After this, your local LumenFlow runtime emits sessions, events, evidence and telemetry to:

  • POST /api/v1/sessions
  • POST /api/v1/events
  • POST /api/v1/evidence
  • POST /api/v1/telemetry

Minimum supported kernel is 5.0.0; the full-feature kernel is 6.0.0.

A newly connected machine needs an owner's capability approval before it can run work. Sessions, events, evidence and telemetry flow straight away, but requests to claim work are refused until a workspace owner or admin approves the machine's capabilities on the Compute page. The capabilities the machine asks for appear there for review, and it can then run only what you approved.

Step 4 — Call the Sidekick API for actions#

With the workspace API key as bearer, hit any of the workspace-scoped endpoints. The most common is dispatching a Sidekick task:

curl -X POST https://<your-cloud-host>/api/v1/sidekick/chat \
  -H "Authorization: Bearer $LUMENFLOW_CONTROL_PLANE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{ "role": "user", "content": "List today open approvals" }],
    "threadId": "<thread-uuid>"
  }'

Other workspace-scoped endpoints under /api/v1/:

  • /sidekick/briefings/run — kick off an objective briefing
  • /sidekick/objectives — read or update objectives
  • /workspaces/<id>/jobs — list jobs
  • /workspaces/<id>/knowledge — manage workspace memory
  • /approvals plus /approvals/<id>/resolve — approval queue
  • /agents — agent fleet
  • /runtime/sessions — runtime sessions
  • /memory/nodes plus /memory/sync — workspace memory

All workspace endpoints carry an X-LumenFlow-Control-Plane-SDK-Contract-Version header back so SDK clients can detect contract drift.

Creating a Job directly#

For automation that names a specific playbook or event instead of chatting, POST /api/v1/jobs (and its alias POST /api/v1/intent) create a governed Job without going through the chat surface:

curl -X POST https://<your-cloud-host>/api/v1/jobs \
  -H "Authorization: Bearer $LUMENFLOW_CONTROL_PLANE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <a-key-you-generate-per-attempt>" \
  -d '{
    "workspace_id": "<workspaceId>",
    "kind": "area-signoff",
    "required_capabilities": ["local-tool-exec"],
    "params": { "site": "north-yard" },
    "callback_url": "https://your-app.example/lumenflow-result",
    "callback_secret": "a-shared-secret-you-generate"
  }'

kind names what you're asking for; required_capabilities is what a runtime must be approved for to run it — they are never inferred from each other. Whether the Job runs as a connector action against one of your connected apps or as managed output is resolved automatically from what you send (a connector-shaped params.invocation, or a capability you already have a connected app for): a managed_artifacts Job runs on the hosted runtime, and any other capability set runs on a Connected Compute node that advertises it. You never name an executor. Whatever you send in params (capped at 64 KiB of JSON) reaches your runner as structured data on the assignment it claims — never re-parsed out of a description — so it can read a field like params.repo_url directly and hand back a field like params.correlation unchanged in its result.

A managed_artifacts Job is real work, not a draft placeholder: the hosted runtime generates the artifact and reaches status: "succeeded", with result_envelope.artifact_refs pointing at what it produced. It's billed the same way a Sidekick chat turn is billed — as a normal model call against your workspace's configured model, no separate compute charge.

callback_url gets one signed POST when the Job finishes, signed with callback_secret so you can verify it — the two must be sent together, since there's no way to sign a callback without a secret. Omit both and poll GET /api/v1/sidekick/jobs/<job_id> instead. Idempotency-Key makes retries safe: resend the same key with the same body and you get back the original Job instead of a duplicate; resend it with a different body and the request is refused.

Listing the connector catalogue#

To render a connector picker without hard-coding provider names, GET /api/v1/sidekick/catalog returns the catalogue and the kinds your key can add. It needs a workspace key with read scope; an inference-only key is refused.

curl "https://<your-cloud-host>/api/v1/sidekick/catalog?availability=ready" \
  -H "Authorization: Bearer $LUMENFLOW_CONTROL_PLANE_TOKEN"

entries is ordered by provider; each has kind (channel, tool_connection or event_source), display_name, availability (coming_soon, limited or ready), auth_kind, its capabilities and risky_capabilities, a setup_hint_kind, a source (built-in, operator-added or workspace-added; a workspace-added entry is visible only to its own workspace) and a connect_mode (attempt, owner_form or operator_only) that says whether a connector needs an operator to connect it. Starting a connection with a key is not available yet. No credentials or endpoints are ever returned. Filter with kind, availability and source; an unknown value is a 400 with code invalid_filter.

install_kinds lists the kinds a workspace can add, each with its install route and installable_by_key. When it is false, send the user to an operator: that kind needs a signed-in member.

Listing the connections an end user holds#

GET /api/v1/sidekick/tool-connections returns the tool connections visible to one scope. It needs a workspace key with read scope; an inference-only key is refused, and so is an app key (the app-key scope for this route is not available yet).

curl "https://<your-cloud-host>/api/v1/sidekick/tool-connections" \
  -H "Authorization: Bearer $LUMENFLOW_CONTROL_PLANE_TOKEN" \
  -H "X-Tenant-Subject: <your end user's id>"

Send X-Tenant-Subject (the same opaque id you use on Jobs) to list one end user's connections. Leave it out to list only the connections that belong to the app itself. An end user never sees another end user's connections, and a connection that belongs to someone else is not an error, it is simply not in the list. Each connection has id, connectorKey, provider, status (connected, needs_reconnect or revoked), enabled, accountLabel, grantedScopes, revision, createdAt and updatedAt. No token, account id or handle is ever returned. Filter with connectorKey and status; an unknown status is a 400 with code invalid_filter, and a malformed X-Tenant-Subject is a 400 with code invalid_tenant_subject. This route only lists: connecting an end user's account from your app is not available yet.

Receiving results by webhook#

When the Job reaches a terminal state, callback_url receives one delivery chain: the first attempt arrives within seconds of completion; failed attempts are retried with backoff up to 6 attempts, then dead-lettered (never retried past one attempt on a 4xx). The body is the same result envelope GET /api/v1/sidekick/jobs/<job_id> returns. Three headers ride the request:

HeaderContents
X-Lumenflow-Signaturet=<unix seconds>,v1=<lowercase hex HMAC-SHA256 of "t.body">, keyed with your callback_secret
X-Lumenflow-TimestampUnix seconds when this attempt was sent — equal to the t inside X-Lumenflow-Signature
X-Lumenflow-Delivery-IdStable per delivery attempt-chain — the same value on every retry of the same result

Verify it before trusting the body — recompute the HMAC yourself and reject anything that doesn't match, and reject a stale timestamp (more than 5 minutes from your own clock) so a captured request can't be replayed later. The header always carries exactly one v1 value today, signed with the one callback_secret your Job was created with — there's no secret rotation to handle yet.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyLumenflowCallback({ secret, rawBody, signatureHeader }) {
  const match = /^t=(\d+),v1=([0-9a-f]+)$/.exec((signatureHeader ?? "").trim());
  if (!match) return false; // wrong shape — reject
  const [, t, providedHex] = match;
  const ageMs = Date.now() - Number(t) * 1000;
  if (!Number.isFinite(ageMs) || Math.abs(ageMs) > 5 * 60 * 1000) {
    return false; // stale or clock-skewed — reject
  }
  const expectedHex = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(providedHex);
  const b = Buffer.from(expectedHex);
  return a.length === b.length && timingSafeEqual(a, b);
}

Python

import hashlib
import hmac
import re
import time

def verify_lumenflow_callback(secret: str, raw_body: bytes, signature_header: str) -> bool:
    match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]+)", (signature_header or "").strip())
    if not match:
        return False  # wrong shape — reject
    t, provided_hex = match.group(1), match.group(2)
    if abs(time.time() - int(t)) > 5 * 60:
        return False  # stale or clock-skewed — reject
    expected_hex = hmac.new(
        secret.encode("utf-8"),
        f"{t}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(provided_hex, expected_hex)

Use the exact raw request body bytes (not a re-serialized JSON.stringify/json.dumps of the parsed object) when recomputing the HMAC, or a byte-for-byte-identical payload will still fail to verify.

Step 5 — Consume the engagement-projection API (read-only, cross-project)#

The engagement-projection surface is the mission control model. You provision an engagement in cloud — a curated read-only view onto a workspace shaped for an external consumer — and issue them an engagement token. They then read from a fixed surface that the workspace admin can introspect via auto-generated OpenAPI per engagement.

# Get the OpenAPI spec for this engagement (auto-generated)
curl https://<your-cloud-host>/api/v1/engagements/<engagement-slug>/openapi.json \
  -H "Authorization: Bearer <engagement-token>"

The 13 endpoints under /api/v1/engagements/<slug>/:

EndpointReturns
/overviewEngagement summary
/workWork units plus delivery state
/capabilitiesCapability inventory
/agentsAgent fleet
/architectureArchitecture read-model
/approvalsApproval queue (read-only)
/evidenceEvidence stream
/eventsPaginated event log
/events/liveServer-Sent Events live stream
/documentsDocument index
/snapshotsPoint-in-time exports
/embed-tokenSigned embed token (for iframe-style embeds)
/openapi.jsonAuto-generated OpenAPI spec for this engagement

Every response goes through a visibility-policy filter — the engagement owner controls what each engagement token can see; raw workspace state is never exposed without explicit projection.

Step 6 — Embed an engagement view#

For dashboards inside your own product (a customer portal, a partner console), issue a signed embed token and iframe the returned URL:

curl -X POST https://<your-cloud-host>/api/v1/engagements/<slug>/embed-token \
  -H "Authorization: Bearer <engagement-token>" \
  -H "Content-Type: application/json" \
  -d '{ "surface": "overview", "ttlSeconds": 900 }'

Response includes an embedUrl and expiresAt. The signed URL carries the visibility scope and renders LumenFlow's engagement UI inline in your product.

Step 7 — Server-to-server webhooks#

LumenFlow can push events outward when something changes — a job completes, an approval lands, an evidence record is anchored. Configure delivery targets per workspace; cloud signs each payload with HMAC so your receiver can verify provenance.

See /api/v1/engagements/<slug>/events for the canonical event catalog and the per-engagement webhook configuration UI in the dashboard.

Verifying the connection#

Once workspace.yaml is in place and your local runtime starts emitting:

  • The dashboard /observe page shows the first runtime session appear within 30 seconds (the default sync_interval).
  • /api/v1/health from your bootstrap returns { status: "ok", workspace: "<workspaceId>" }.
  • The workspace Recent activity card surfaces the first event.

If nothing appears, check that $LUMENFLOW_HOME/secrets/<workspaceId>.env exists for this project (or that LUMENFLOW_CONTROL_PLANE_TOKEN is present in the environment that runs the local runtime), and that the workspace API key has not been regenerated since cloud connect last ran. After a regeneration, re-run npx lumenflow cloud connect --force ... in that project.

Auth model recap#

Token kindIssued viaScopeRevoke
Workspace API keyPOST /api/v1/workspaces/<id>/api-keyWorkspace-scoped actions under /api/v1/, limited to the key's scopes (read, write and any opt-in scope named at issue)DELETE on the same endpoint; other instances may keep accepting it for up to 15 seconds
Inference keyPOST /api/v1/workspaces/<id>/inference-keysOnly inference:invoke, for governed model callsDELETE /api/v1/workspaces/<id>/inference-keys/<keyId>
App keyPOST /api/v1/workspaces/<id>/app-keysThe declared app scopes only, never read or writeDELETE /api/v1/workspaces/<id>/app-keys/<keyId>; refused on its next use
Org service tokenPOST /api/v1/orgs/<orgId>/service-tokensOrg-wide reads (read), plus the opt-ins workspaces:manage and members:manageDELETE /api/v1/orgs/<orgId>/service-tokens/<tokenId>
Runner onboarding grantPOST /api/v1/workspaces/<id>/runner-onboarding/grantsThe runner:onboard.* scopes, for runner onboarding endpoints onlyDELETE /api/v1/workspaces/<id>/runner-onboarding/grants/<grantId>; refused on its next use
Engagement tokenPer-engagement provisioning in dashboardOne engagement, read-only, visibility-policy filteredEngagement settings page; immediate
Signed embed tokenPOST /api/v1/engagements/<slug>/embed-tokenSingle surface, single TTLExpires on TTL; revoke parent engagement to invalidate early
Dashboard JWTDashboard sign-in on /dashboardUI actions onlySign-in session expiry

The workspace API key is the strongest credential — treat it like a service-account key. Engagement tokens are safer to share externally because their visibility is bounded by the engagement projection configuration.

Where to go next#

  • Approval Workflows — gate risky actions behind human review
  • Agent Fleet Management — manage runtime agents per workspace
  • Webhook Integrations — push events outward to your own systems
  • Sidekick Tools Reference — the ~33 named tools Sidekick can dispatch from chat
  • Connected Compute — pairing your own Linux or Windows machine to run Sidekick work uses a separate, short-lived pairing code rather than this workspace API key