npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@segmentstream-cloud/ontology

v0.68.0

Published

Define a SegmentStream workspace app's ontology: entities, relationships, roles and actions, written with Effect.

Readme

@segmentstream-cloud/ontology

The backend of a SegmentStream workspace app, written with Effect. The ontology/ folder of the workspace repository declares entities, their properties, the relationships between them, the roles and the actions people and agents may take. From that one definition, the SDK builds the manifest, the tables in the workspace's database and the HTTP service under /api/ontology. Every read and every action requires a permission that the caller's roles grant, and every change to a record goes through an action and is kept in history.

TypeScript definitions, handler records and SDK calls use camelCase: jobTitle, assignedOwnerId, createdAt, and changeStatus. The SDK converts ontology names, select identifiers and declared action input fields to snake_case for the generated REST API, version 10 manifest and storage. TypeScript helpers and options stay camelCase. JSON property contents, provider payloads, opaque action results and historical change names remain data and are preserved. formatManifest serializes the canonical wire contract shared by REST, storage and future transports.

When calling REST or MCP directly, use the snake_case wire name:

| TypeScript action | REST path | MCP tool | | ------------------ | --------------------------------------------------------- | ----------------------------------- | | createDraft | /api/ontology/expense/actions/create_draft | expense_create_draft | | attributePayroll | /api/ontology/bank_allocation/actions/attribute_payroll | bank_allocation_attribute_payroll |

ontology check lists canonical wire action names. MCP tools/list gives exact tool names; describe action references use action:expense.create_draft. Incorrect casing and close typos produce hints with a suggestions array for authenticated callers who may run the suggested actions (MCP unknown-tool errors put it in error.data). Hints never execute an action or create an alias; retry explicitly with the suggested name.

Changes

Workspace deployment is automatic on main pushes by default. To accumulate changes for an explicit release, commit .segmentstream/deployment.json with { "schemaVersion": 1, "trigger": "manual" }. Cloud keeps storing and indexing source, while segmentstream deploy explicitly deploys current main with its existing authorization and idempotency checks. The configuration takes effect from current main without an ontology migration. Removing the file or selecting "push" restores automatic deployment; no scheduler is added, and already-started deployments continue.

For an existing hourly release process, the optional productionWindow setting in the same file restricts explicit production admission to configured UTC minutes. Outside that window, create and retry return deployment_window_closed with the next opening time; no attempt starts. This adds no scheduler or preview restriction.

SDK 0.33.0 requires ActionContext.principal.identities. Code that invokes an action handler directly, including unit tests, must provide it (use identities: [] when there are none), plus createdVia and authenticatedVia. Identity facts use provider, subject, optional username, and verified: true. The general Principal and prepare context do not include this field.

Install

npm install @segmentstream-cloud/ontology [email protected]

effect is a peer dependency. Install the exact version above: each SDK release supports one Effect version.

ontology/ is a package of its own. The SDK is an ES module, so ontology/package.json must set "type": "module".

SDK 0.37.0 writes manifest version 9, which adds workflows (see Workflows). Version 8, since SDK 0.23.0, added the measures of the model. defineOntology({ measures }) declares measures of the model: arithmetic over measures of entities, m.ref("acmeSales.ordered"), and over other model measures, m.ref("planAttainment"). align names the properties that stand for one another across the entities a measure reads, or null for an entity without a counterpart, such as a current snapshot without a date:

defineOntology({
  entities: [AcmeSales, AcmePlans],
  measures: {
    planAttainment: m.divide(m.ref("acmeSales.ordered"), m.ref("acmePlans.plannedUnits"), {
      align: [{ acmeSales: "orderDate", acmePlans: "month" }],
      unit: "ratio",
      description: "Ordered units over planned units for the same months.",
    }),
  },
});

Aggregate requests name a measure of the model by its bare name, plan_attainment, beside entity.measure; a filter must reach every entity it reads unless its alignment says the entity has no counterpart, and reading it requires reading each of those entities. check refuses aggregates inside a measure of the model, unknown references, cycles and alignments that do not name every entity once with properties of one type. Entity measures take no align.

SDK 0.17.0 writes manifest version 7. It adds m.avg, m.min and m.max; a where option on every aggregate, so that a measure reads only the rows its conditions accept (m.sum("amount", { where: [{ property: "status", operator: "equals", value: "won" }], description })); and balanceOver, which names the date a measure's rows are snapshots over, so that requests group by it or filter it to one day. Aggregate requests take the notEquals, notIn, gt, gte, lt, lte, contains, startsWith, endsWith and inPeriod operators, the hour, quarter and year granularities and a timezone. See ontology aggregates.

SDK 0.14.0 lets the app's own agent call the service as a paired member (see Agents). Its migration adds the agent's runtime tables.

SDK 0.18.0 lets blob properties store any file. accept is optional and can restrict a property to exact MIME types or ranges. Existing explicit restrictions keep their meaning. See File storage.

SDK 0.12.0 adds warehouse replication enabled by default for all stored business entities, fields and business relationships. Existing history captures immutable snapshots and the service wakes the publisher after commits. Upgrade and redeploy to activate it; workspace configuration can disable replication or exclude individual entities, fields and relationships. See setup and recovery.

SDK 0.13.0 writes manifest version 6. Relationships may target warehouse-backed entities, from stored or warehouse-backed entities, matched by value through on, and unique is allowed on warehouse-backed properties as an unenforced assertion. Upgrading: 0.12 accepted an explicit onDelete, or optional: true, on a warehouse → stored relationship; 0.13.0 check refuses both, so remove them. See ontology aggregates.

SDK 0.13.0 also serves aggregates. Upgrade notes:

  • POST /api/ontology/aggregate, the MCP tool aggregate and, for each entity with measures, POST /api/ontology/<entity>/aggregate answer measures of the model as the caller. The service checks <entity>:read on every entity a request touches, then forwards it to SegmentStream with SEGMENTSTREAM_APP_CREDENTIAL. Previews and local development have no credential and answer 503 aggregate_unavailable.
  • m.countDistinct(property) counts the distinct values of a text, email, url, select, integer, date or timestamp property.
  • Stored entities may declare measures, computed over their replicated records; the built-in User cannot.
  • aggregate is reserved for the route and the MCP tool: rename an entity with that name before upgrading.
  • Under signIn: { policy: "open" }, anyone who signs in holds member, so check refuses a member whose wildcard (*:read, the default, or *:*) would let strangers read warehouse-backed entities through aggregates. Grant those to member by name (acme_sales:read), or keep the invited policy.

See ontology aggregates and ontology analytics.

File storage

Use p.blob({ description: "Original source file." }) to retain a file's original bytes. Without accept, a blob property accepts every concrete MIME type, including custom formats and application/octet-stream. p.blob() also defaults to all files, though ontology checks require a description. The manifest publishes this policy as accept: ["*/*"].

Restrict a property when its business rules require a format:

content: p.blob({
  description: "Original POS report or bank statement.",
  accept: [
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "application/vnd.ms-excel",
    "text/csv",
    "text/tab-separated-values",
  ],
  maxBytes: 10 * 1024 * 1024,
}),

accept takes MIME types, not extensions. Matching ignores case; image/* accepts image types and */* accepts all types. Empty lists, duplicate entries, invalid MIME types and parameters such as ; charset=utf-8 are rejected during ontology checks. BLOB_CONTENT_TYPES lists common types as a convenience; it is not a global allow-list.

| Source format | MIME type | | --------------------- | ------------------------------------------------------------------------- | | XLSX | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | | XLS | application/vnd.ms-excel | | CSV | text/csv | | TSV | text/tab-separated-values | | JSON | application/json | | TXT | text/plain | | DOCX | application/vnd.openxmlformats-officedocument.wordprocessingml.document | | ZIP | application/zip | | Unknown binary format | application/octet-stream |

Uploads still require a valid concrete MIME type, a nonempty file within the property's maxBytes and the 250 MiB service limit, available workspace quota and action permissions. A refusal for a platform limit is an action_rejected body with limit: storage_quota with used_bytes, requested_bytes and quota_bytes; pending_uploads with pending and max; or member_attachments with live and max, so an app or agent can explain it in its own words. Completion verifies the stored MIME metadata, exact byte count and upload-session binding. PDF, JPEG and PNG retain their prefix-signature checks; plain text retains its UTF-8 prefix check. Other formats are stored as opaque bytes, without parsing, conversion, archive extraction or malware scanning. A MIME declaration does not prove what a file contains. Downloads use attachment disposition; only JPEG and PNG may request inline display. Agent attachment promotion honors the same accept and maxBytes restrictions.

Server-side OntologyBackend extensions can use blobs.target(uploadId, principal) to inspect a session's persisted entity, action, property, record ID or create intent before continuing an upload. It checks the session owner, workspace branch, deployed manifest and current member permissions without provider calls or writes. It is not an HTTP endpoint and returns no storage key or transfer grant. Custom blob adapters may omit this optional method; extensions requiring target inspection must then deny the operation.

Upgrade the ontology SDK to 0.18.0 or later, update any existing accept list that should allow more types, then check, migrate and redeploy the ontology. The app SDK storage client 0.13.1 or later uses application/octet-stream when the browser supplies no MIME type. REST callers should supply that type themselves for unknown formats.

See blob storage for upload tickets, transfer grants and retained revisions.

SDK 0.56.0 adds server-side file transfers through Files. In an action, declare files: [uploads.target(self, "content")], stage a stream in prepare, then attach the returned ticket in handler. This uses the same private bucket, upload sessions, permissions, quotas and completion checks as browser uploads. Files.open streams an authorized retained revision for sending to another provider. Use files: [] when preparation only reads files and has no connections. Callers of staging actions provide an idempotency key; workflow action calls derive a stable key from the run, step and input when omitted. Streams stay within the step; carry tickets and references between steps. See server-side file transfers for the API and recovery rules.

Connections retain buffered get/post behavior. To read a large response, use connection.stream({ method: "GET", path }) inside Effect.scoped and consume its body as an Effect Stream before closing the scope. Request bodies remain capped at 2 MiB.

Define the ontology

An action with hints: { connectionsReadOnly: true, destructive: false } may read providers in prepare and write local records in handler. All its connection clients enforce GET/HEAD/OPTIONS without bodies or method overrides. MCP treats these provider reads as closed-world; destructive or irreversible local writes still require confirmation. This differs from readOnly: true, which requires a prepare-only action and permits no local writes.

// ontology/lead.ts
import { action, ActionRejected, entity, p, rel, role, User } from "@segmentstream-cloud/ontology";
import { Effect, Schema } from "effect";

export const sales = role("sales", {
  description: "Works leads to a pilot.",
  permissions: ["lead:read", "lead:win"],
});

export const Lead = entity("lead", {
  id: "ent_01J9Q4M8ZK",
  label: "Lead",
  plural: "Leads",
  title: "email",
  description: "A person who asked for a pilot.",
  properties: {
    email: p.email({ unique: true, description: "The address they submitted." }),
    status: p.select(
      [
        { value: "new", label: "New", description: "Nobody has replied yet." },
        { value: "won", label: "Won", description: "They became a customer. Final." },
      ],
      { default: "new", managed: true },
    ),
  },
  relationships: {
    owner: rel.one(User, {
      optional: true,
      inverse: "ownedLeads",
      onDelete: "setNull",
      phrase: ["is owned by", "owns"],
      description: "The team member responsible for the next step.",
    }),
  },
}).actions((self) => ({
  win: action({
    description: "Mark the lead as won.",
    input: Schema.Struct({ lead: self.ref }),
    // Requires lead:win, the default: <entity>:<action name>.
    handler: ({ input }) =>
      Effect.gen(function* () {
        const lead = yield* self.get(input.lead, { forUpdate: true });

        if (lead.status === "won") {
          return yield* new ActionRejected({ message: "The lead is already won." });
        }

        yield* self.update(lead, { status: "won" });
      }),
  }),
}));
// ontology/index.ts
import { defineOntology } from "@segmentstream-cloud/ontology";

import { Lead, sales } from "./lead";

export default defineOntology({
  entities: [Lead],
  roles: [sales],
  // What every active app user may do. Without it, members may only read: ["*:read"].
  member: { permissions: ["*:read"] },
  // Who may become a member by signing in: only invited people (the default), or anyone ("open").
  signIn: { policy: "invited" },
});
  • entity(name, { … }) declares an entity. id is its stable identity; do not change it.
  • p.text, p.email, p.url, p.integer, p.number, p.boolean, p.date, p.timestamp, p.select and p.json declare properties. managed: true keeps a property out of the generated update action.
  • rel.one and rel.many declare relationships. Between stored entities, rel.one adds a key and rel.many a join table. With a warehouse-backed end, rel.one declares one on pair and is matched by value. User is the built-in entity for app users and service principals; AgentChat (agent_chat) is the built-in entity for the group chats of the app's agent (see Agents). Every ontology includes both. Text feedback defaults on with built-in AppFeedback; files follow the non-feedback-blob policy (or an explicit opt-in) and add AppFeedbackFile (see App feedback).
  • role(name, { description, permissions }) declares a role: a named set of permissions. member is built in: every active app user holds it, with the permissions of defineOntology({ member }).
  • .actions((self) => ({ … })) adds actions. Each action has a description, an input schema and an Effect handler. It requires the permission <entity>:<action name>, or the one its permission names.
  • signIn: { policy } decides who may become a member by signing in. invited, the default, admits only people whose email a member already has, from an invitation or a role grant; anyone else is answered 403 not_invited. open makes anyone who signs in a member holding only member. The manifest carries the policy. Explicit app opt-in (never a platform or starter default): channel admission is independent: signIn: { policy: "invited", channels: { telegram: "open" } } lets private channel senders become email-less members. Missing channels require pairing; email policy: "open" never opens a channel. CHANNEL_PROVIDERS lists supported providers (telegram). Every configured channel requires a matching chat.<provider> adapter; object values are reserved. Open channels require explicit member.permissions: entity wildcards and generated create/update/delete grants (including grants through entity :*) are errors. Named shared reads warn for review; public catalogs are valid. The hint is “move staff reads into a named role”. Actions such as updateMine must check ownership with principal.id. Closing a channel stops new registrations; existing members keep working. Blocking is sticky and independent of activation; deleting a member releases identities. signIn.onNewUser: "person:ensureMine" optionally names an app-owned onboarding action. It must exist, accept {}, be runnable by member, declare hints.idempotent: true, and have no prepare or external connections. Its handler must have no external effects. The runtime runs it as the new user with only member permissions after admission commits, before the first model turn or Mini App session. Handler writes and its completion marker commit together, under a five-second deadline. Failure logs diagnostics and leaves it pending for retry on the next eligible contact; admission still succeeds. The service shares one action admission gate between onboarding and ordinary actions, reserving database capacity for other requests before either kind of handler acquires a connection. Invited/paired-only members never run this hook. The platform supplies mechanism; the app owns channel policy, grants, hook content, notice text, and lower limits. Nothing defaults to open. check(config, { agentConfig }) warns if open channels lack firstContact.notice or limits when agent config is supplied. The ontology CLI cannot see that config and says those checks are skipped.
  • defineOntology checks the definition and builds the manifest. It throws InvalidOntology with every diagnostic when the definition has errors. Entities, actions and select options need a description.

Workflows

A workflow is a durable process over time, written with the Vercel Workflow SDK: it can wait, retry a step, run several actions and call connections. The destination decides the home, not the data source: work that outputs tables is a job in pipeline/; records change through actions; work that runs on its own over time and outputs records, paged provider reads included, is a workflow in ontology/workflows/. Declare one per file with workflow and cron from @segmentstream-cloud/ontology/workflows, and list them in defineOntology({ workflows }):

// ontology/workflows/remind-stale-leads.ts
import { cron, p, run, workflow } from "@segmentstream-cloud/ontology/workflows";
import { Effect, Schema } from "effect";
import { Lead } from "../lead";

async function remindStaleLeads(input: { olderThanDays: number }) {
  "use workflow";
  const leads = await findStale(input.olderThanDays);
  for (const lead of leads) await remind(lead.id);
}

async function findStale(olderThanDays: number) {
  "use step";
  const before = new Date(Date.now() - olderThanDays * 86_400_000).toISOString();

  return run(
    Effect.map(
      Lead.find({ where: { status: "new", updatedAt: { lt: before } }, limit: 100 }),
      (leads) => leads.map((lead) => ({ id: lead.id })),
    ),
  );
}

async function remind(leadId: string) {
  "use step";
  return run(Lead.actions.addNote({ lead: leadId, body: "No movement for three days." }));
}

export default workflow("remindStaleLeads", {
  description: "Write a reminder note on every lead that has not moved.",
  role: "reminders",
  input: Schema.Struct({ olderThanDays: p.integer({ description: "Days without movement." }) }),
  run: remindStaleLeads,
  triggers: [
    cron("weekdayMorning", "0 8 * * 1-5", {
      description: "Every weekday at 08:00 UTC.",
      input: { olderThanDays: 3 },
    }),
  ],
});
  • Import what the file uses at its top level from @segmentstream-cloud/ontology/workflows, which re-exports p for the input, and Schema from effect. The Workflow SDK bundles a workflow file's top-level imports for a runtime without Node built-ins; this entry, and entity and action declarations, import none, while the package root does. Steps may use anything: the SDK strips their bodies from that bundle.
  • workflow(name, { description, role, input?, run, triggers?, connections? }) only records the declaration; check and the evaluation of index.ts never call run, and the "use workflow" and "use step" directives are inert strings until the Workflow SDK's compiler builds the service (ontologyWorkflows() in ontology/nitro.config.ts, below). Keep workflow files in the directories it compiles (./workflows by default): a run the SDK did not compile is logged as ontology.workflow.not_compiled when the service starts, and starting it answers 503 workflow_not_compiled.
  • name is camelCase and unique; it names the workflow's user. description is required.
  • role is a declared role other than member, and not one that grants *:*. The runs hold what it and member grant.
  • input is a Schema.Struct; omitted means {}. run receives the decoded input in TypeScript names, and as its second argument { startedBy }: the trigger, the member, or the platform (with the person it acted for) that started the run.
  • triggers lists what starts the workflow on its own. Each carries a kind: cron, action or webhook. cron(name, expression, { description, input? }) takes five fields in UTC, each *, numbers, ranges, steps after * or a range (*/5, 0-30/10; not 5/15) or lists, with at most one of day-of-month and day-of-week restricted and no month or day names, as Vercel accepts them. Its input is written in TypeScript names and must decode with the workflow's input. Trigger names are camelCase, <workflow>.<trigger> on the wire (remind_stale_leads.weekday_morning).
  • after(name, target, { description, when?, input? }) starts the workflow after an action commits: target is an action handle (Lead.actions.changeStatus) or an entity handle (Lead, any of its actions). when and input are synchronous functions of the event ({ action, actionId, at, principal, input, result, edits }) and the record ({ record, before, after }, typed as the entity's records); input omitted means {}. changed(field, { from?, to? }) is a when for the common case. A workflow reacts to an action, not to a field. The manifest records the kind, name, description, entity and action, never the functions. At most 20 action triggers per ontology; check reports invalid-trigger, trigger_action_unknown, trigger_function_async and workflow_trigger_limit_exceeded.
  • webhook(name, { description, signature?, headers? }) starts the workflow for each delivery a provider pushes to /api/webhooks/<name>, whose wire name is unique across the ontology. The workflow's input is webhookDelivery (body, contentType, query, headers, receivedAt) and it has only webhook triggers. Without signature, the URL ends in a token derived from the workspace's webhook secret; with { scheme: "stripe", secret }, each delivery's signature is checked against that workspace secret through SegmentStream. headers lists up to 10 request headers the delivery carries. The route answers 200 once the run started, 404, 401, 413 or 415 to refused deliveries and 503 when the run could not start. Production only. check reports the workflow_webhook_* codes; at most 20 webhooks per ontology.
  • connections lists the connections the steps call directly; each must be in defineOntology({ connections }). Connections declared on an action stay that action's.
  • At most 50 workflows. check reports workflow_role_unknown, workflow_role_is_member, workflow_role_grants_nothing (a warning: with member's grants, the role grants no action), workflow_role_wildcard (the role grants *:*), workflow_input_invalid, workflow_cron_invalid, workflow_cron_never_fires (a warning: no listed month has the day), workflow_run_missing (run is not a function), invalid-workflow, invalid-property (a property option its constructor does not take), and the usual name, duplicate and description checks.

Code inside a step

Inside a "use step" function, run(effect) runs an Effect with the ontology's runtime as its services, bound to the run's workflow user. Entity handles work as in an action handler: Lead.find and Lead.get read, Lead.create, update, upsertBy, delete, link and unlink run the generated actions, and Lead.actions.addNote(input) runs the authored action with its input in TypeScript names; so do the workflow's connections. Every read and action goes through the same path as the HTTP routes: it is checked against the workflow user's permissions, validated, and recorded in history under the workflow user. Batch writes (createMany, updateMany, upsertMany) are refused in steps; run an action that writes the batch. In production, workflow roles can explicitly grant agent_chat:post_message and agent_chat:preview_message for attributed chat delivery and its destination preview. Preview deployments refuse both actions; agent tasks cannot post to chat on either target.

A step calls a model through Vercel AI Gateway with the AI SDK (ai), outside run: the deployment authenticates with its Vercel project identity, so the workspace holds no key. Give each model call a step of its own that returns its result, and store the result with an action in the next step, since a retried step calls the model again. A workspace's spent Gateway budget refuses the call with 402. The ontology SDK does not wrap these calls.

run resolves with the effect's value, which the step returns, so keep it JSON-serializable. A failure rejects: a typed failure of the handles (NotFound, Conflict, ActionRejected, PermissionDenied, ConnectionError) becomes an Error named after it with its message and the request id; anything else becomes a generic message with the request id, and the service logs the full cause as ontology.workflow.step.failed. The Workflow SDK records the failed step and retries it, except a refusal that would repeat (PermissionDenied, ActionRejected, an invalid input), which is marked fatal. A read the role does not grant fails the step. Calling run in the "use workflow" body or outside a run throws WorkflowStepRequired. run reads the step from the Workflow SDK's process-wide step context and never imports the SDK, which a deployed function need not be able to resolve.

A retried step runs its actions again: the SDK has no idempotency for actions, so write steps whose actions tolerate a repeat (an upsertBy, a check before a write), as the example's submitLead does with its submission id.

The binding: when the ontology service builds its routes it installs, on globalThis, the runtime that serves this process's runs, with the services the routes run with; the step callback builds the service first when this instance has not. Inside a step, run reads the run from the Workflow SDK's step context (getWorkflowMetadata()), finds the declared workflow whose compiled run has the message's workflow id, and checks it against the run's own record in the world (runs.get(runId).workflowName, read once per run), refusing a step whose workflow is not its run's; a run the world does not show yet fails the step, which is retried. It then resolves the workflow's user and provides the runtime bound to it. No token is involved; connection calls carry the callback request's Vercel OIDC token (x-vercel-oidc-token), else the deployment's.

Agent tasks

A workflow hands the workspace's agent a prompt with agentTask from @segmentstream-cloud/ontology/workflows, in its "use workflow" body, and waits for the agent's final answer without holding compute. It is the agent members reach in chats, with the same instructions, models, tools and sandbox, acting as the workflow's own user: it sees and does exactly what the workflow's role and member grant, and ontology actions run without confirmation, since no person is present. Keep the role narrow: a prompt may carry untrusted text.

import { agentTask, workflow } from "@segmentstream-cloud/ontology/workflows";

async function draftReply(input: { message: string }) {
  "use workflow";
  const prompt = await describeMessage(input.message); // a step that reads records
  const result = await agentTask({ prompt, timeoutMs: 15 * 60_000 });

  if (result.status === "completed") {
    await saveDraft(input.message, result.text); // a step that runs an action
  }
}
  • prompt is 1 to 32,768 characters, all of it the workspace's: nothing is added to it. timeoutMs defaults to 900,000 (15 minutes) and is 60,000 to 3,600,000. Invalid options fail the workflow with AgentTaskOptionsInvalid, which names the option, and a call outside a "use workflow" body (in a step, or outside a workflow) with AgentTaskWorkflowRequired; neither is retried.
  • It resolves with { status: "completed", text, truncated }, the agent's final text; { status: "needs_input", text, truncated }, its question, when the agent would need a person; { status: "failed", reason } with agent_unavailable (the deployment has no agent, its agent predates tasks, or the run is outside production), busy (the workspace has 10 live tasks; the agent was not called) or agent_failed (its turn failed, its cost cap included); or { status: "timed_out" }. Text beyond 65,536 characters is cut and marked truncated.
  • A task cannot post to chat in this release: agent_chat.post_message is refused for a task whatever the role grants. The answer reaches people through the workflow's later steps.
  • At least once: a start step retried after the agent accepted the task hands it over again, and the agent ends the earlier session. Write prompts and later steps that tolerate a repeat.
  • Tasks run only in production, where the agent's runtime tokens are valid. Locally and in previews agentTask resolves with agent_unavailable.

agentTask creates a Workflow SDK hook and runs its start step, one of two steps the package ships; the Workflow SDK finds and compiles them through @segmentstream-cloud/ontology/workflows with the workflow's own. The step records the task in _agent_tasks under the hook's token, mints its secret (32 random bytes, base64url, of which only the SHA-256 is stored; it is never returned, logged or written to the run's event log) and posts { task_id, secret } to the agent's POST /eve/segmentstream/v1/tasks at https://$VERCEL_PROJECT_PRODUCTION_URL, with the service's own Vercel OIDC token. 202 starts the task and 404 ends it as agent_unavailable; no answer, 5xx, and a 409 while the task is still pending fail the step, and the retry hands the pending task over with a new secret. A retry finds a started or ended task and does not hand it over again; any other answer ends the task as agent_unavailable and fails the step without a retry. The workflow then races the hook against a sleep until the task's deadline, timeoutMs after the start created it, so start-step retries count against the timeout. The agent's report through the task routes (see Agents) ends the task, the first report winning, and resumes the hook; if the sleep wins, the second step times the task out unless it already ended and returns its outcome, so the workflow's result is always the task's stored outcome. Each start deletes the tasks whose deadline passed 30 days ago.

Runtime and routes

ontology/nitro.config.ts lists ontologyWorkflows() from @segmentstream-cloud/ontology/nitro in modules, in place of the SDK's own workflow/nitro:

import { ontologyRoutes, ontologyWorkflows } from "@segmentstream-cloud/ontology/nitro";
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  compatibilityDate: "2026-09-11",
  serverDir: false,
  routes: ontologyRoutes("./server/handler.ts"),
  plugins: ["./server/plugin.ts"],
  modules: [ontologyWorkflows({ dirs: ["./workflows"] })],
});

It adds the SDK's transform to the server build and, before each build, bundles the workflows of dirs and their steps into Nitro's build directory, with /api/ontology as the SDK's base path and ontology as its queue namespace, both written into the generated entrypoint at build time (no environment variable). It mounts one route in the ontology service's own server bundle, /api/ontology/.well-known/workflow/v1/flow: since Workflow SDK 5 the flow handler runs the steps itself, and background steps come back on the workflow topic. The route answers only a POST queue delivery of the ontology's queue (ce-vqsqueuename, or the local world's x-vqs-queue-name, starting with __ontology_wkf_workflow_) at its exact path, a trailing slash refused; anything else is a 404. Before the first step of an instance it builds that instance's ontology service through Nitro's own fetch of /api/ontology/status; a failure fails the delivery for the queue to retry. On Vercel the route gets its own function with maxDuration: "max" and the queue trigger __ontology_wkf_workflow_*; runs start on the same queue (start with the namespace ontology). The app's own workflows use the SDK's default topics and the agent's use topics of its own, so none of them receive another's messages. Nitro states that routes with queue triggers are not reachable from the web; that is still to be confirmed on a deploy. If they are reachable, the queue-header and run-record checks still refuse a request that is not the ontology's own delivery.

Webhooks are not supported yet: no webhook route is mounted, since this version of the SDK derives a webhook's token from the run. Hooks resumed from server code with resumeHook work, as agent tasks' do.

server/plugin.ts calls installWorkflowRuntime() from @segmentstream-cloud/ontology/server at startup: until the flow handler loads and sets the base path from the build, it sets it to /api/ontology, so that runs started first call back there, and it registers the Workflow SDK, loaded on first use, for the workflow routes. It also sets WORKFLOW_QUEUE_NAMESPACE to ontology in the service's process, which serves only the ontology's queue: resumeHook takes no namespace and enqueues the resumed run on the one this variable names. Only that entry imports the Workflow SDK: the package root does not, so a process that bundles the service on another Workflow SDK major carries none of it, and neither does @segmentstream-cloud/ontology/workflows, which the Workflow SDK bundles with every workflow file.

| Route | Who | What | | --------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------- | | GET /api/ontology/workflows/:workflow/triggers/:trigger | Vercel cron, Authorization: Bearer $CRON_SECRET | Starts the workflow with the trigger's input; 202 { runId }. | | POST /api/ontology/workflows/:workflow/start | Holders of workflow:<workflow>:start; the platform | Starts it with { "input": … } in wire names; 202 { runId }. | | GET /api/ontology/workflows | Any caller; each sees the workflows it may read | The declarations, each trigger with its path and nextAt. | | GET /api/ontology/workflows/:workflow/runs | Holders of workflow:<workflow>:read, the platform | The Workflow SDK's runs, newest first; ?limit and ?after. | | GET /api/ontology/workflows/:workflow/runs/:runId | As above | One run with its steps. |

CRON_SECRET is read from the service's environment; without it, the trigger routes refuse every request. Workflow permissions, workflow:<workflow>:start and workflow:<workflow>:read (workflow:*:read, workflow:<workflow>:*), are granted by roles like action permissions; only a permission that starts with workflow: grants them, so *:* and *:read do not. The platform authenticates with a workload token whose subject is platform:<workspace id>; it is admitted to these routes only, and its start requests may name startedBy, which the run receives. The deployment's runtime callers hold what the member they act for holds, and never start runs. Vercel may deliver a cron invocation more than once, and start does not deduplicate: a trigger's workflow tolerates a repeated run. A body that is not JSON answers 400.

Every route takes the workflow's TypeScript name or its wire name, remindStaleLeads or remind_stale_leads, and so does the trigger segment; the start body's input is in wire names, as an action's input is.

Pages take ?limit (1–100, default 20) and ?after, the previous page's nextCursor, which is null on the last page. Success bodies use camelCase; errors are the service's usual { error, detail, request_id }. tests/fixtures/ontology-workflow-service.json at the repository root pins every body, for the CLI and backend-next.

Run records come from the Workflow SDK's world (runs.list, runs.get, steps.list) with status, times and error. The world takes pages of at most 100: runs pages are capped there, and a run's detail follows the steps' cursor up to 1,000 steps, with stepsTruncated: true beyond. When the world cannot list a run's steps, the detail answers the run with steps: null and the service logs ontology.workflow.steps.failed with the cause; inputs and outputs are not returned, since the world stores them serialized.

Workflow users

Each workflow runs as its own keyless service user: name = the workflow's wire name, identity_provider segmentstream-ontology-workflow, subject workflows:<workspace id>:<workflow name>, roles = exactly the declared role, no API key. The migration that applies the ontology creates it: applyMigration given the workspace's workspaceId runs reconcileWorkflowUsers in the transaction that records the manifest, under the migration lock, also on a no-op plan, and segmentstream-ontology migrate does the same when SEGMENTSTREAM_WORKSPACE_ID is set. It deactivates the users of removed workflows and reactivates redeclared ones. A run before the deploy creates a missing user (resolveWorkflowUser), but a run never changes roles: the deployed database is the authority, and a user whose roles are not exactly the declared role is refused (role_mismatch) until a deploy reconciles it. Runs that are in flight on an older deployment when a deploy changes the workflow's role fail fatally at their next step, since their deployment declares the old role. The user never holds member as a role; every action re-reads its roles in the action's transaction and refuses any assignment other than the declared role.

Batch writes in actions

Stored entity handles expose createMany(records, options?), updateMany([{ id, values }], options?), and upsertMany({ on, records, returning? }) inside authored action handlers. The action remains the permission and commit boundary; a batch does not run other authored actions for its rows.

For example, an action can write sales plan lines by a declared compound unique key:

const PlanLine = entity("planLine", {
  id: "ent_plan_line",
  label: "Plan line",
  plural: "Plan lines",
  title: "planCode",
  description: "A sales plan decision for a period.",
  properties: {
    planCode: p.text({ description: "The plan code." }),
    period: p.date({ description: "The period start." }),
    target: p.number({ description: "The agreed target." }),
  },
  indexes: [{ properties: ["planCode", "period"], unique: true }],
}).actions((self) => ({
  setTargets: action({
    description: "Set targets for several plan lines together.",
    input: Schema.Struct({
      lines: Schema.Array(Schema.Struct(self.properties)),
    }),
    handler: ({ input }) =>
      self.upsertMany({
        on: ["planCode", "period"],
        records: input.lines,
        returning: "summary",
      }),
  }),
}));

Both result modes return { created, updated, unchanged }, whose counts sum to the input length. The default, returning: "records", also returns items in input order: { index, status, record }, with status equal to "created", "updated", or "unchanged". returning: "summary" omits items; the enclosing action still returns its normal edits. These types are exported as BatchRecordResult<Record>, BatchSummaryResult, and BatchResult<Record>.

Creates retain normal defaults and validation. Updates patch distinct IDs: omitted fields remain unchanged, and explicit null follows the property's nullability. An unchanged patch preserves timestamps and attribution and adds no history or replication work. Upsert keys must be a nonempty list of ordinary properties matching a declared unique property or compound unique index. Each input explicitly supplies non-null key values and enough values to create a record; existing records receive only supplied non-key fields. Duplicate IDs or upsert keys fail the batch.

Batch values support ordinary properties and stored rel.one assignments. Blob and rel.many assignments are rejected; use the existing single-record methods for those fields. Warehouse-backed and platform-owned built-in entities are not batch write targets. OntologyWrites exposes the same operations with an explicit entity and snake_case field names; entity handles convert authored camelCase names and select values.

Each call accepts at most 200 records and 2 MiB of UTF-8 JSON input, including record values, update IDs, and upsert key fields. One action may attempt at most 500 rows across nonempty batch calls, including failed calls its handler catches. Empty input returns zero counts and, in record mode, empty items, without SQL. Every nonempty batch uses one savepoint inside the action transaction. A failure rolls back all of that batch's records, history, replication references, and edits; a handler may catch it and continue. A later uncaught action failure also rolls back earlier successful batches.

Existing ActionRejected, Conflict, and NotFound errors may include batch: { indices, fields? }: zero-based input positions and safe wire field names. Batch calls are never automatically retried. Checkpoint a loader only after confirmed action success; replay after a lost response is safe only when the complete action is explicitly idempotent, including its other writes and side effects. Generic upsert alone does not establish that guarantee.

Fresh aggregates in actions

Stored entity handles expose aggregate({ measures, groupBy?, where }) to action handlers and prepare. It computes measures the entity declares, with their warehouse meaning, over the records where matches, in the action's transaction, so it counts the handler's own writes:

const totalsOf = (invoice: string) =>
  Effect.gen(function* () {
    const [totals] = yield* InvoiceLine.aggregate({
      measures: ["net", "tax"],
      where: { invoiceId: invoice },
    });

    return totals; // { net: "1501", tax: "300" }
  });

Rows hold the groupBy fields and each measure as a decimal string or null; without groupBy there is exactly one row. where is find's query language and must look records up through an index: eq or in on id, a unique property, a rel.one key or the first field of a declared index. It aggregates every matching record, with no row limit, so its cost follows the set the indexed condition selects. Aggregates lock nothing: lock the record the decision depends on with forUpdate. Balance measures, model measures and workflow steps stay with the warehouse aggregate. See fresh aggregates inside an action.

Permissions

me (also GET /api/ontology/me) includes authentication: { kind, client, scope } and attribution. OAuth provenance carries the registered client's ID/name and the grant's scope; its permissions remain restricted by that scope. Attribution uses stored names through workspace-authored chat.attribution.template (default {name}), with {client} naming the registered OAuth client (empty for runtime/service callers). Client names are registration metadata, without a claim of brand ownership. Ordinary browser/app members, member API keys and pipeline workloads receive null attribution and cannot use that provenance to post. No bearer token or client secret appears in me.

Since 0.27.3, verified agent runtime calls cannot discover or execute agent invocations, including the legacy agent_chat.send_message relay and reserved agent_start/get/update/cancel operations. MCP, REST, discovery/catalog exports, OpenAPI and attachment promotion apply the same policy. OAuth, service and ordinary member callers retain their usual permission checks when runtime authentication is configured. This preparation does not advertise or enable exact chat posting.

A permission is <entity>:<verb>. The verbs are read, the generated create, update, delete, link and unlink, and the names of authored actions. A role may use * for the entity, the verb or both: lead:*, *:read, *:*. An entity wildcard never covers the built-in agent_chat, app_feedback or app_feedback_file: reading or changing them needs a permission that names them, such as agent_chat:read.

  • Reads require <entity>:read: lists, queries, records, history, both entities of a relationship traversal, and every entity an aggregate touches. include and a filter through a relationship require read on the related entity. A missing permission answers 403 with <permission> is required.
  • Each action requires one permission. The generated upsert_by_<property> requires both <entity>:create and <entity>:update.
  • A principal's permissions are the union of its roles' permissions in the ontology. Members hold member and their assigned roles; an API key holds exactly the roles it was created with; a workflow's runs hold the workflow's role and what member grants. A role the ontology no longer declares grants nothing.
  • The catalog, describe, graph, openapi.json and me need only authentication; status, sign-in/check and the docs page need none. ?as=me lists only the actions the caller may run.
  • check fails on a role without permissions, on a permission whose entity or verb does not exist, and on allow, which version 0.2 replaced. It warns on an action no role may run and on *:*.

Discover the ontology over MCP

/mcp uses stateless Streamable HTTP: POST returns JSON-RPC results, while authenticated GET requests accepting text/event-stream receive an immediate SSE comment and 15-second heartbeat comments. The GET stream carries no JSON-RPC messages or session ID. It closes after 45 seconds with a reconnect hint; clients can reconnect independently to any instance. Set mcpStreamMaxDurationMs in createOntologyService to change that lifetime, keeping it below the deployment's function limit with room for startup and authentication. The generated Vercel service enables Fluid and Nitro response streaming without overriding maxDuration; the conservative default also fits a 60-second function limit. GET and POST share bearer authentication and Origin rules. DELETE is unsupported because there are no sessions.

Since 0.27.0, agents use describe without arguments for a compact entity index and discover for keyword, literal substring, or fuzzy metadata search. Inspect entity:lead, action:lead.change_status, or measure:sales.revenue refs with describe for complete definitions. Entity inspection includes all property metadata, relationships, permitted generated and authored actions with their input contracts, and computable measures. Related entities stay references.

Index and search results carry hash and next_cursor; pass the cursor as after only when more results are needed. Complete inspection is not paginated. An optional expected_hash detects a changed deployment. Stored schema visibility does not grant permission to read records or run operations.

Every MCP client receives the permitted operation tools directly through standard tools/list. Large external catalogs are paginated: follow nextCursor by sending it as params.cursor until it is absent. Each page contains at most 128 tools and normally fits within 500,000 UTF-8 bytes; an individually larger tool gets its own page. Descriptions and input schemas are complete, and annotations and trusted metadata are preserved. Optional output schemas are omitted.

The server no longer exposes ontology_search_tools, ontology_describe_tool, or ontology_call_tool. Clients own tool search and call the selected original name with tools/call. Refresh connector catalogs after upgrading, and remove mcp.toolListing from workspace configuration. Existing discover and describe inspect domain entities, actions, and measures; they are not substitutes for MCP tool discovery.

Each page rechecks permissions. Cursors belong to the current catalog and permission scope; restart listing if either changes. Verified in-app runtimes receive the same direct operations in one page because Eve 0.85 currently reads only the first listTools response. This preserves running agents during the upgrade without exposing the retired helpers to either caller type.

The MCP ontology://describe resource is the first index page as JSON. ontology://catalog remains an explicit full manifest export; REST descriptions and CLI catalog rendering retain whole-catalog behavior. Custom prompts expecting the old full no-argument MCP description should switch to progressive inspection or explicitly request the full export.

Serve the ontology

List and query endpoints accept select to avoid fetching unused base fields: GET /api/ontology/lead?select=email,status or a query body with { "select": ["email", "status"] }. An explicit empty list returns ids only. Selected pages include id and projected: true; omitted fields are absent. include still adds complete related records under the same read permissions. Filters, scopes and cursor ordering apply before projection and may use fields not selected. Without select, the complete record response is unchanged. Authored action reads (Entity.find, get) remain complete records.

import { configFromEnv, createOntologyService } from "@segmentstream-cloud/ontology";

import ontology from "./index";

const service = createOntologyService(ontology, configFromEnv());

// Give every request under /api/ontology to the service.
export function handleOntologyRequest(request: Request): Promise<Response> {
  return service.handler(request);
}

GET /api/ontology/docs serves a Scalar API reference over openapi.json. A browser signed in to the app authenticates with the app's Neon Auth session cookie (__Secure-neon-auth.session_token), which the service exchanges at Neon Auth for the session's token and verifies like a bearer token. A bearer token always takes precedence. The cookie authenticates a POST only when its Origin is the app's own.

handler(request) takes a web Request and returns a Response. dispose() closes the database pool. configFromEnv() reads these variables (ENVIRONMENT_VARIABLES, and the optional NEON_AUTH_ENVIRONMENT_VARIABLES). It throws OntologyServiceConfigError when one is missing or invalid, and never repeats the database URL.

| Variable | Purpose | | ------------------------------------- | -------------------------------------------------------------------------------------------- | | SEGMENTSTREAM_ONTOLOGY_DATABASE_URL | Pooled connection string of the ontology_app role. | | SEGMENTSTREAM_URL | SegmentStream origin. It issues and verifies app tokens, and receives aggregates over https. | | SEGMENTSTREAM_APPLICATION_ID | App tokens must be issued for this application. | | SEGMENTSTREAM_WORKSPACE_ID | The workspace of the app. | | SEGMENTSTREAM_ORGANIZATION_ID | The organization of the workspace. | | NEON_AUTH_BASE_URL | Optional. The workspace's Neon Auth URL; its origin issues users' tokens. | | NEON_AUTH_JWKS_URL | Optional, set with the base URL. The keys that verify those tokens. | | SEGMENTSTREAM_APP_CREDENTIAL | Optional. The workspace app credential; without it, aggregates answer 503. | | SEGMENTSTREAM_RUNTIME_OIDC_ISSUER | Optional. The team's Vercel OIDC issuer, for the app's agent. | | SEGMENTSTREAM_RUNTIME_OIDC_AUDIENCE | Optional, set with the issuer. The audience of the team's tokens. |

The platform sets these variables in the workspace deployment, the Neon Auth pair once the workspace has Neon Auth and the runtime pair on deployments with an agent. With the runtime pair, Vercel's VERCEL_PROJECT_ID must be set too.

App preview deployments

A preview requires the emitted workspace-app-previews-v1 runtime capability and workspace-app-preview-workflows-v1 for workflow execution. The platform also probes the installed SDK's fixed capability profile and expiry checks on the locked dependencies before app code is exposed; preview support is never inferred from an unpublished release number.

configFromEnv() reads SEGMENTSTREAM_DEPLOYMENT_TARGET (production when omitted), SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID and SEGMENTSTREAM_PREVIEW_EXPIRES_AT. Explicit previews require a deployment UUID, a valid ISO deadline and child Neon Auth bindings. The returned deployment observation and /api/ontology/status report the deployment identity, expiry and capabilities. Preview admission uses the fixed preview profile even if a custom configuration supplies broader capability values. SDK failures use DeploymentAdmissionError.code; workspace code owns their presentation.

Preview services accept child Neon tokens and email sessions, checking the exact child issuer even when keys were copied from production. Legacy app and production workload tokens, opaque API keys, OAuth/channel sessions and Cloud federation are refused. Ontology CRUD, files and manual workflow starts retain their normal user permission checks. Action/change triggers and inbound webhooks can start workflows; ontologyWorkflows() compiles and mounts their queue handler. Trusted workspace code can make external network requests and cause real external side effects. Managed connections, after-commit hooks, agents (including workflow chat delivery and agent tasks), pipelines and replication remain unavailable. Scheduled business cron triggers are never registered, and direct calls to their trigger routes are refused even with a cron secret.

Workflow execution requires the runtime VERCEL_DEPLOYMENT_ID system variable. New preview runs carry immutable segmentstream.deploymentTarget and segmentstream.deploymentId attributes; run reads, steps, hook resumes and decoded queue callbacks require that exact app deployment UUID and the Workflow SDK's matching provider deployment ID. Missing or copied production provenance is refused. Production cannot read or resume preview runs. A stable alias is not run ownership. Every admission checks the deadline, including cached sessions, warm callbacks and workflow steps. Work already admitted can finish; preview expiry does not isolate or cancel arbitrary workspace code.

Platform workflow starts, reads and recovery use short-lived platform:<workspace id> workload tokens with environment=preview, both deployment IDs equal to the exact child UUID, the request's immutable candidate origin, and a token expiry no later than the preview deadline. These tokens grant no other ontology operations. Recovery calls POST /api/ontology/workflows/sweep with that token to retry persisted _workflow_starts; preview GET sweeps and cron-secret admission are refused. The platform must schedule this recovery separately without registering workspace business crons.

Each preview gets a fresh SEGMENTSTREAM_WEBHOOK_SECRET for unsigned URL tokens, never the production value. Signed webhooks and Meta registration retain their declared validation and use the backend's existing verification endpoints. Only those calls carry the child app credential as x-segmentstream-preview-credential plus x-segmentstream-ontology-deployment-id, runtime Vercel OIDC, organization and request headers. The backend verifies the credential's exact live deployment and the deployed secret declaration; named signing secret values are never injected into the preview.

Before exposing a verified child database, the provisioner runs:

import { preparePreviewDatabase } from "@segmentstream-cloud/ontology";

yield * preparePreviewDatabase({ sql });

This transactional, repeatable preparation clears SDK-owned API keys, OAuth/federation sessions and tokens, agent pairings/identities/confirmations, workflow starts/tasks, upload sessions, file action replay results and replication delivery state. It disables copied replication while preserving users, roles, persistent member blocks, channel policy, business records, histories and retained file revisions. Older databases with fewer SDK tables are supported. It never changes neon_auth or any provider-owned schema: child Auth invalidation, origins, email delivery and secrets remain the provisioner's responsibility. Call it only on a verified, unexposed child, before new preview workflows can start; never repeat preparation on a serving child or recovery would lose that child's pending starts. Copied production starts and tasks must be cleared before exposure.

The preview SEGMENTSTREAM_ONTOLOGY_STORAGE binding additionally requires deploymentId matching this deployment and the provisioner's verified productionBranchId, which must differ from branchId. Endpoint validation still binds storage to that child branch. The provisioner must verify that DB, Auth and storage all belong to the same child and issue child-only credentials; SDK self-consistency checks cannot prove provider ownership. The runtime rebinds copied file revision metadata to that child's provider and bounds signed transfer grants by the preview deadline. Production storage bindings need neither new field.

Authentication

Every route but status and sign-in/check takes Authorization: Bearer …:

  • Neon Auth token. The app's users sign in with the workspace's Neon Auth (Managed Better Auth). The service verifies their JWT: EdDSA, issued by the origin of NEON_AUTH_BASE_URL, against the keys at NEON_AUTH_JWKS_URL. The token names the user (sub, email, name) and carries no roles. The email counts only when Neon Auth verified it.
  • SegmentStream app token. Still accepted while workspaces move to Neon Auth. The service picks the verifier by the token's issuer; a token from any other issuer is a 401. A verified app-token email also resolves an existing Neon Auth member, without replacing the member's Neon identity. The service reads that member's current roles and active state.
  • API key. ssk_…, held by a service user.
  • Workload token. Issued by SegmentStream for production operations and the exact preview workflow operations described above. Its subject decides who it is: pipeline-workers:<workspace id> is the pipeline worker; platform:<workspace id> is SegmentStream acting for the console and the CLI, admitted only to the workflow routes and refused everywhere else. Any other subject is a 401; a workload token never resolves as a member or the agent.

Sign-in finds the user linked to the token's provider and subject, else links the member invited with the same email, else follows the ontology's signIn policy: invited answers 403 { "error": "not_invited", "detail": "Ask your administrator for an invitation." }, open creates a member. Deactivated members are refused. Each sign-in sets the user's last_sign_in_at, at most once per 15 minutes, so the user's history records it.

Members without an email can be managed by their user UUID in the console and CLI: ontology member grant|revoke|deactivate|reactivate MEMBER. Find the UUID with ontology member list. The platform's POST …/ontology/members/roles accepts either email (including new invitations) or memberId (existing members only). The SDK's updateMemberRoles(ontology, { id, grant, revoke }) uses the same role writer as email updates. Deactivation preserves the member's roles and identities.

GET /api/ontology/me answers who the caller is, for the app to render what they may do: { user: { id, name, email, kind, active }, roles, permissions, authentication, attribution }. It needs only authentication; services get email: null.

POST /api/ontology/sign-in/check with { "email": "…" } answers, before the app emails anyone, whether the address may sign in and how. Under invited, an active member who has signed in with Neon Auth gets { "allowed": true, "method": "code" }, an active member who has not gets method: "link" (Neon Auth sends codes only to existing accounts; a sign-in link creates the account), and anyone else, deactivated members included, gets { "allowed": false }. Under open, every address gets link. It reads nothing else and changes nothing. A malformed address answers 400. Each service instance allows a client address (x-real-ip, else the first x-forwarded-for hop) ten checks a minute, then answers 429 rate_limited with retry-after. checkSignIn(ontology, email) is the same lookup for code that holds a SQL client.

App feedback

Text feedback is enabled when feedback is omitted. Every authenticated human member can submit, read