Blueprints

Explanation — what & why

A blueprint is an app model as configuration. One file declares everything a small Vectros app needs to exist — the record schemas, a least-privilege access profile, a service principal, optional seed data, and optional roles — with stable identifiers so applying it twice converges instead of duplicating (the loader always reconciles server-side via ?upsert=true, so re-applying the same blueprint is always safe). A schema whose typeName is already claimed by a different owner — e.g. a second blueprint modeling the same conceptual type — must declare basedOn (see the schema reference below).

Blueprints exist to collapse "stand up a new app's data layer" from a sequence of API calls into a single reviewable artifact. You write the file once; the CLI's bootstrap and blueprint-test commands turn it into provisioned infrastructure and a working scoped key (see cli.md).

Two boundaries define what a blueprint is and is not:

  • It describes format, not trust. The @vectros-ai/blueprints package owns the format and its structural validation only. The security boundary — the scope gate that confines a blueprint to data-plane access — lives in the CLI binary. A blueprint is untrusted input; the binary is the trust boundary.
  • It is data-plane only. A blueprint can request scopes on records, schemas, search, documents, folders, and inference — nothing else. Control-plane scopes are hard-rejected when the CLI applies it.

This doc is the format reference the blueprint walkthroughs build on. For narrated, end-to-end builds, see the walkthroughs (getting-started, clinical-intake, agentic-sdlc, second-brain).

How-to

Scaffold, validate, and apply

The full authoring loop runs through the CLI:

vectros blueprint init my-app                  # scaffold ./my-app.blueprint.yaml
vectros blueprint validate ./my-app.blueprint.yaml
vectros blueprint plan ./my-app.blueprint.yaml
vectros blueprint apply ./my-app.blueprint.yaml --tenant test   # provision it, no key (needs CLI 0.23.0+; CI: add --yes --confirm-existing-principal)
vectros bootstrap --blueprint ./my-app.blueprint.yaml           # provision it AND mint a key for your MCP client

You can scaffold from a bundled exemplar with init --from <name>. See cli.md for the lifecycle commands.

A minimal blueprint

Blueprints are authored as YAML or JSON. A minimal one declares a context, one schema, an access profile, and a service principal:

name: my-app
version: 1.0.0
description: A minimal example app.
contextId: my-app
contextName: My App

schemas:
  - typeName: note
    displayName: Note
    indexMode: HYBRID          # HYBRID | SEMANTIC | TEXT
    fields:
      - { fieldId: title, fieldType: string, required: true, searchable: true }
      - { fieldId: body,  fieldType: string, searchable: true }
    lookupFields: [title]        # extra exact-match index(es). Do NOT list externalId:
                                 # it's the record's first-class id (sent top-level on
                                 # each record), with its own finder — declaring it as a
                                 # field or lookup is rejected.

accessProfile:
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]

servicePrincipal:
  externalId: my-app
  displayName: My App

indexMode, the per-field validation/renderHints, and the schema-level capabilities/active/ownership fields are all optional — the example stays minimal on purpose. Note the deliberate absence of records:d in allowedActions: least privilege is the authoring default.

Reference — what a blueprint can express

Top-level fields

FieldRequiredMeaning
nameyesStable blueprint id — the --blueprint <name> selector and idempotency key.
versionyesBlueprint version string.
descriptionyesHuman-readable description.
contextIdyesThe app context the profile and key bind to. 3–31 chars; lowercase letter first, then lowercase letters/digits/dashes.
contextNamenoHuman-readable context name; defaults from name.
schemasnoThe record/surface schemas to provision (see below).
accessProfileyesThe least-privilege scope the bootstrap mints for the blueprint's own key.
servicePrincipalyesThe service principal the key is bound to.
seednoDeterministic seed records.
rolesnoReusable, multi-clause scope rules.
identitiesnoDeclared principals (see the glue caveat below).
issuersnoTrusted third-party IdP issuers to register, for BYO-IdP token exchange (see below).
namespacesnoEntity-namespace registrations to declare (see below).
scriptsnoThe versioned JS source a trigger's scriptRef runs (see below).
triggersnoRules that invoke a script on a schema record's CREATE/UPDATE/DELETE event (see below).
identityProjectionClaimsnoWhich captured IdP claim names get projected, read-only, onto access profiles in this context (see Issuers below).
inputsnoInstall-time variables (resolved with --set/--values).

Schemas

Each schema entry declares a record type:

FieldMeaning
typeNameThe record type name.
displayNameHuman-readable label.
indexModeHYBRID (keyword + semantic), SEMANTIC, or TEXT.
fields[]Field definitions (see below).
lookupFields[]Fields to index for direct lookup. Bare field name (equality), or { fieldName, unique?, rangeEnabled?, sortBy?, allowOverflow? } — or, for a composite lookup over 2–3 fields together, { fieldNames: [...], sortBy?, allowOverflow? } (see Composite lookups below). rangeEnabled adds ordered from/to/prefix queries (use it for dates/sequences/scores; the order is lexical, so it's wrong for ordinal enums). sortBy sets the order an equality lookup returns — createdAt (default), lastUpdated, or another field on the schema. You can also range over that sort key (sortFrom/sortTo on a value lookup — e.g. one session's records since a timestamp). The sorted field does not need to be required: records that carry no value for it are ordered ahead of those that do, and are never inside a bounded window. sortBy and the sorted field's type are both migration-locked, so choose them when you declare the lookup; an array or object field cannot be a sortBy target. Each schema has 7 fast equality slots (range lookups use a row, not a slot; ownership ids + externalId ride their own); an 8th equality lookup needs allowOverflow. The index shape is migration-locked — pick it deliberately. A sensitive field may be an equality (blind-index) lookup, never rangeEnabled, and never a sortBy target. Up to 10.
allowedSurfaces[]Which typed surfaces may bind the schema: record, document, user, entity (identity entities in any namespace — org, client, or one you register). Defaults to [record].
capabilities.auditHistoryWhether writes emit version history (platform default on).
capabilities.triggersEnabledWhether records of this schema fire trigger rules. Defaults to false, and is the opt-in a schema must declare before a trigger may fire on it — a blueprint declaring a trigger against a schema without it is refused at plan time. Independent of auditHistory.
activeWhether the schema accepts new records.
userId / scopesSchema-level ownership defaults. scopes is an array of <namespace>:<value> entries (e.g. org:acme), the same grammar records/entities use.
basedOnId of an existing schema this one customizes, required when a schema named typeName already exists in this context under a different owner. Omit when this is the first schema under that name (it becomes that name's shared base — root/unscoped credential only, no userId/scopes). Must point directly at the base (one hop); immutable once set. Mirrors the platform's basedOn schema field (data-model/reference.md).

Each field definition supports:

Field keyMeaning
fieldIdField name.
fieldTypeField type (e.g. string, array, reference).
required / searchable / filterablePer-field flags.
enumValues[]Allowed values for an enumerated field.
validationValidation rules: required, minLength/maxLength, min/max, pattern, email/url/phone, step/multipleOf, minItems/maxItems.
renderHintsUI hints: label, widget (text/textarea/select/date/checkbox), order, section, helpText, displayField (mark the record's headline column — at most one per schema).
sensitiveMarks the field as sensitive: it is redacted/destroyed at write time, blind-indexed for lookups, excluded from the search index, and masked on read unless a token carries the reveal scope.
inlineKeep the field on the record row when the payload is stored out of line, so it appears in list and lookup projections without includePayload — and so a trigger rule may project it into input.record. Cannot be combined with sensitive.
targetTypeName / targetSurface / targetField / cardinalityOn a reference field, the typed link target. targetTypeName and targetSurface are both required — the same typeName can exist on multiple surfaces, so the surface disambiguates the lookup. targetSurface is record, document, user, or the name of an entity-backed namespace — org, client, or one you registered via POST /v1/namespaces (see identity-access/reference.md; the namespace must already be registered and entity-backed, or the schema is rejected at authoring time). targetField defaults to the target's externalId (must be a unique lookup); cardinality = one (default) / many. The target's existence is enforced at write by default.

Composite lookups

A lookup can span 2–3 fields matched together as one index, declared with fieldNames in place of fieldName:

lookupFields:
  - fieldNames: [status, area]   # queried together as one exact-match index
  • Order is fixed at declare time. A query can match a leading run of the declared fields — the first field alone, the first two, and so on — but never a later field alone; declare a separate lookup for that. Redeclaring the same fields in a different order creates an independent lookup that only sees writes made after it was declared, not the history under the old one.
  • Exact-match only. A composite lookup can't be unique or rangeEnabled — declare those as a separate, single-field lookup instead.
  • Record schemas only. allowedSurfaces must be exactly ['record']; document, user, and entity schemas can't declare a composite lookup.
  • Each leg must be a plain, single-valued, non-range field. A leg can't be an array or object field (one sort-key position holds one value), a reference field with cardinality: many, or a field that is itself declared rangeEnabled elsewhere on the schema — any of those would index only one of several values and the conjunction would silently return a subset while looking complete.
  • A leg cannot also be the composite's own sortBy target. Within a fixed tuple every matching record shares the same value for that leg, so the sort payload would be constant and a sortFrom/sortTo window would return either everything or nothing, at double the sort-key cost. Sort by a different field, createdAt, or lastUpdated instead.
  • sortBy and allowOverflow still apply otherwise: sortBy orders results within a group when a query supplies fewer values than the lookup declares (see lookupFields[] above).

Access profile

accessProfile:
  allowedActions: [records:cru, search:r, schemas:r]
  dataScope:
    "scope:org": [org_acme, null]
  • allowedActions — the scopes the minted key carries. Author explicit resource:op forms; coarse verbs and resource:* grant nothing at runtime. Subject to the data-plane scope gate.
  • roleIds — alternative to allowedActions/dataScope: compose the profile from one or more roles this same blueprint declares under roles: (below), instead of an inline clause:
    accessProfile:
      roleIds: [case-handler, hr-admin]
    roles:
      case-handler:
        - allowedActions: [records:r:case, records:u:case]
      hr-admin:
        - allowedActions: [records:r:hr]
    
    The effective grant is each named role's own clauses, concatenated in the order listed — never merged, so each clause keeps meaning exactly what its own author wrote. Mutually exclusive with allowedActions; a roleIds- composed profile carries no dataScope/capabilities of its own — author those on the referenced roles instead. Every id must resolve to a role declared in this same blueprint, and no id may repeat.
  • dataScope — optional ownership binding (userId or scope:<namespace> → value lists; scope:org/scope:client for those reserved namespaces, or scope:<namespace> for one you registered — org/client must be registered too, same as any other). A null element is the documented null sentinel: it additively grants access to tenant-level (owner-less) records in addition to the listed owners. Omitting null restricts the key to the listed owners only — so it will not see tenant-level/seed records.
  • capabilities — optional list of platform capability names (distinct from a schema's capabilities.auditHistory), e.g. capabilities: [member-lifecycle]. This package validates the shape only (non-blank, no duplicates, lowercase kebab-case, no '*') — it does not itself know which names the platform actually grants, since that set is a platform property (six names as of this release: member-lifecycle, forensic-read, context-directory-read, delegate-mint, delegate-principal-stamp, trigger-control-plane-grant — see identity-access/reference.md). This field parses and validates; whether it has any effect depends on whether the tool applying your blueprint reads and forwards it. Each role clause below accepts the same field.

Service principal, seed, roles

servicePrincipal:
  externalId: my-app
  displayName: My App

seed:
  - typeName: note
    externalId: seed-welcome
    fields: { title: Welcome, body: Created by the bootstrap loader. }   # externalId is the seed's top-level field above, NOT a payload field

roles:
  editor:
    - allowedActions: [records:cru, search:r]
      dataScope:
        userId: ['${{ self.userId }}']
  • servicePrincipal — the principal the minted key binds to.
  • seed — deterministic records (keyed by externalId for idempotency). vectros bootstrap writes them unless you pass --no-seed; vectros blueprint apply writes them only when you pass --seed.
  • roles — a map of role id → ordered clauses. Each clause is an (allowedActions, dataScope) pair, evaluated per-clause at runtime. Roles are identity-agnostic and reusable; bind them to principals with vectros access grant --role <id>. Every role clause is also gated to the data plane, so a role cannot be a control-plane back door around accessProfile.
  • ${{ self.* }} placeholders are a runtime per-principal sentinel, resolved per-request by the platform. They are only valid inside a role clause's dataScope; using one anywhere else is an authoring error.

fragments — authoring sugar for a repeated dataScope. A top-level map of name → dataScope, referenced from a clause with dataScopeRef instead of repeating an identical dataScope verbatim:

fragments:
  ownOrg:
    "scope:org": ['${{ self.scope.org }}']
roles:
  case-handler:
    - allowedActions: [records:cru:case]
      dataScopeRef: ownOrg
    - allowedActions: [search:r]
      dataScopeRef: ownOrg

dataScope and dataScopeRef are mutually exclusive on the same clause. A dataScopeRef is resolved to its fragment's literal dataScope before anything downstream (the loader, the wire payload it sends) ever sees it — a purely local authoring convenience, never itself provisioned.

roleAssumable — which values a role's holder may become. A top-level map of roleId → grant, naming which values, per scope:<namespace>, a holder of that role may assume via POST /v1/auth/token/assume (identity-access/reference.md):

roleAssumable:
  hr-admin:
    "scope:org": [org_engineering, org_sales]
roles:
  hr-admin:
    - allowedActions: [records:r]

Every key must resolve to a role this same blueprint declares under roles. Its value grammar is narrower than a clause's dataScope: every key must be a namespaced scope:<ns> (the principal — userId — can never be named here), and no value may be null. vectros blueprint plan previews a role's assumable grant alongside its clauses, since it's security-relevant — what a holder of the role may become, not just that the role was declared.

assignableRoles — both accessProfile and each role clause may carry an optional roleId allow-list restricting which named roles that clause may compose into a delegated access profile (roleIds composition). Orthogonal to allowedActions/dataScope: it narrows which roles the clause's authority may hand out, not how much data it reaches. Omitting it means no restriction; an empty list is rejected rather than read as "compose nothing". At most 20 entries, duplicates rejected. Entries are deliberately not resolved against this blueprint's own roles — a clause may legitimately name a role that already exists in the context. Rejected on a roleIds-composed accessProfile, which has no inline clause to carry it. Once a clause carries the field it can compose only the roles it names, so name every roleId the restricted clause still needs before adding it.

Scripts

A blueprint may declare a top-level scripts — a map of script name → { source, declaredInputContract? } — carrying the source a trigger's scriptRef runs, so a blueprint that declares a trigger can ship the code that trigger executes. Applied in-context, before the triggers that reference it: a scriptRef is validated when the rule is declared rather than when it fires, so a rule naming a script with no versions in the target context is refused outright.

scripts:
  notify-assignee:
    source: |
      export default function (input) {
        return { notified: input.recordId };
      }
    declaredInputContract: "{ recordId: string, record: { status: string } }"
  • The map key is the script's name — what a scriptRef.name resolves against. 1–64 characters of letters, digits, _ and -.
  • source — required, non-blank, at most 300,000 UTF-8 bytes. Nothing parses it, here or on the platform, so a syntax error stays invisible until the version first runs.
  • declaredInputContract — optional, at most 50,000 UTF-8 bytes, and descriptive only: it documents the input shape for a human reading the blueprint, and nothing enforces it.

Both caps count UTF-8 bytes, not characters, matching the platform.

There is no path:, deliberately. A shipped script runs under its trigger's declared grant, so it is live authority, and a blueprint is a bundle someone reviews before applying it — keeping the source inside the bundle keeps what runs inside what was reviewed.

You cannot declare a version, and a trigger on a script this blueprint ships must use version: latest — a pin is rejected at authoring time. Versions are assigned by the platform and a stored version is immutable, so a blueprint declares only the current source for a name; a pin would name a version the blueprint cannot know it produced, and would keep running superseded code once the source was edited. A scriptRef naming a script this blueprint does not ship may still pin — that is the "reference a script someone else pushed" case.

Re-applying an unchanged blueprint pushes nothing, and removing a scripts entry does not delete the script — the deliberate asymmetry with triggers below. A script row carries no record of which blueprint pushed it, the stored source is the only copy, and an unreferenced version is inert. Remove one deliberately with vectros scripts delete.

Triggers

A blueprint may declare a top-level triggers — a map of trigger name → declaration, each naming a schema event that invokes a versioned script under an explicitly declared grant. A declared trigger fires: a record write on a schema that opts in via capabilities.triggersEnabled dispatches the rule's script, which runs asynchronously in a sandbox under the grant declared here. So a trigger's allowedActions/dataScope/capabilities, or the roles its roleIds composes, are live authority — author them as narrowly as any other credential's, and review them the same way. Applied per-context, like roles/accessProfile.

schemas:
  - typeName: intake
    displayName: Intake
    capabilities: { triggersEnabled: true }
    fields:
      - { fieldId: status, fieldType: string, inline: true }
triggers:
  on-intake-update:
    firingSource: { schemaName: intake, event: UPDATE }
    scriptRef: { name: notify-assignee, version: latest }
    fields: [status]
    allowedActions: [records:r:intake, records:u:intake]
    dataScope: { "scope:org": ["${{ input.scope.org }}"] }
    manifest: [records.get, records.update]
  • fields — required. The record fields projected into the script's input.record, and on an UPDATE firing into input.previous. fields: [] is the explicit "project nothing" declaration (the script still receives input.recordId); it is required rather than defaulted so that projecting nothing is something you state. An entry may name only a field the schema keeps inline — inline: true, filterable: true, or a lookup field (a leg or its sortBy target) — and never one marked sensitive. record and previous together are capped at 224 KB serialised; a firing that exceeds it is not run.
  • ⚠️ A rule that declares any fields must be able to read what it fires on. Its grant must hold a records:r clause covering the firing schema's type — a grant with no read at all is rejected — and that clause may not carry a literal data_scope constraint (a ${{ input.userId }} or ${{ input.scope.<namespace> }} placeholder is fine, since it resolves to the firing record's own value).
  • firingSource.schemaName + firingSource.event (CREATE / UPDATE / DELETE) — schemaName must name a typeName this same blueprint declares.
  • scriptRef.name + scriptRef.version — an exact integer, or the literal "latest". For a script this blueprint ships, latest is the only accepted value (see Scripts above).
  • A grant — roleIds (composed from this blueprint's roles) or an inline allowedActions/dataScope pair, mutually exclusive, the same XOR shape accessProfile uses.
  • manifest (optional) — host-object verbs (e.g. records.get) further narrowing what the script may call on top of what the grant permits. Never widens it.
  • dataScope values may use ${{ input.<dim> }}, resolving to a dimension stamped on the record whose change fired the trigger. Valid only inside a trigger's dataScope; ${{ self.* }}, ${{ under.self.* }} and ${{ member.* }} are rejected there, because they resolve against the trigger principal's identity rather than the author's.

Removing a trigger de-provisions it. Triggers are the one block an apply reconciles by absence as well as presence: delete an entry and re-apply, and its rule and the access profile behind it are deleted, so a trigger you removed cannot keep acting under a grant that outlived it. Its service principal is kept, so re-adding the same trigger reattaches to one identity. Triggers created outside this blueprint are never touched. Nothing else a blueprint declares is deleted by removing it.

Issuers

issuers:
  - issuerId: my-idp
    issuer: https://idp.example.com
    jwksUri: https://idp.example.com/.well-known/jwks.json
    audience: my-vectros-app
    contextId: my-context

A blueprint may declare top-level issuers — trusted third-party IdP issuers to register for BYO-IdP token exchange, each { issuerId, issuer, jwksUri, audience, contextId, subClaim?, emailClaim?, userinfoUri?, restrictedToDomain?, selfSignupPolicies?, capturedClaims? }. userinfoUri points at the IdP's OIDC userinfo endpoint: when a presented token misses emailClaim (access tokens generally don't carry email under OIDC unless the IdP was specifically configured to add it), Vectros falls back to calling userinfoUri with that token as the bearer credential and reads emailClaim from its response — omit it to leave the fallback disabled. Unlike schemas/accessProfile/roles (applied under a per-context token), issuers are applied in the loader's bootstrap-token phase, alongside app-context/service-principal creation — tenant-wide provisioning config that needs the bootstrap credential's owner-only authority. (issuer, audience) must be globally unique across every tenant (an entry without restrictedToDomain claims its pair when it is verified, not when it is registered), unless the entry sets restrictedToDomain — a domain already verified for your account, written as a lowercase DNS name such as acme.com — which scopes the uniqueness to that domain: a token then matches only when its hd claim equals it (Google Workspace emits hd natively; other identity providers need a claims-mapping rule that emits a claim named exactly hd), so scoping an issuer whose users sign in without one locks them out.

Each entry's contextId must equal the blueprint's own contextId — an issuer is a trust anchor, so a blueprint may only attach one to the context it actually provisions. @vectros-ai/cli 0.16.0+ handles a blueprint whose issuers target a context other than the bootstrap credential's own transparently: the credential re-mints itself, pinned to the blueprint's own context, specifically to register those issuers — no extra authoring step is needed on your part. The loader always registers (idempotent by issuerId) and then reconciles: editing an already-registered issuer's subClaim, emailClaim, userinfoUri, restrictedToDomain, selfSignupPolicies or capturedClaims and re-running bootstrap now converges on the declared values. Leaving restrictedToDomain out of a re-apply keeps an existing scope; it does not clear it. ⚠️ The trust-anchor fields (issuer, jwksUri, audience, contextId) stay immutable, and editing one on an already-registered issuer now fails the whole apply with the platform's 400 rather than being silently ignored as it once was. Issuer status is not blueprint-authorable at all — suspend or reactivate an issuer through the API. ⚠️ From API/SDK 0.45.0, an issuer a blueprint registers without restrictedToDomain starts pending_verification and accepts no token until its registrant proves control of the identity provider with the API's verifyIssuer call (identity-access/how-to.md); an issuer that already existed is unaffected. An entry that sets restrictedToDomain is active at once: the domain you verified for your account is what it proves, and it only ever matches that domain's users. Verifying needs a real login from the IdP after a human configures a claim rule there, so vectros bootstrap (or vectros blueprint apply) cannot finish it: it still completes and exits 0, says pending verification beside the issuer, and ends its output, on stderr, with the challenge and the exact vectros issuers verify command (@vectros-ai/cli 0.23.0 and later). --require-verified-issuers turns a pending issuer into exit 4 (a one-line notice follows the challenge) for a pipeline that must not go green over it, and an issuer whose challenge expired while pending is reported as unverifiable, with the delete command; nothing is deleted for you. Adding restrictedToDomain to an issuer that is still awaiting verification is refused by the platform (400), so the apply fails: verify the issuer first, or delete it and apply again. (Registering, reading, updating, or deleting an issuer directly against the API, outside a blueprint, stays confined to the calling credential's own context — see identity-access/reference.md for the full field grammar, every error code, and the token-exchange contract those issuers serve; identity-access/explanation.md for the concept and the flow end to end.)

capturedClaims names additional OIDC claim names — beyond emailClaim, which keeps its own field — to read from that issuer's tokens on every successful exchange and store as your tenant's golden identity-provider-asserted copy for the signed-in user. Not a fixed set: name whatever your provider actually asserts. Each is read from the verified token first, falling back to userinfoUri (when configured) only for names still missing. Omit to capture nothing beyond email — unchanged behaviour for every existing blueprint. At most 20 entries of at most 64 characters each; a duplicate is rejected rather than deduplicated.

The top-level identityProjectionClaims is the other half: it declares which of those captured names are projected, read-only, onto access profiles in this blueprint's app context, where they surface as identityProjection on the profile. Capture and projection are separate opt-ins — capturing stores a claim against the user, projecting exposes it on a profile — and declaring a name no issuer captures simply never fills. A profile's projection fills once, the first time a sign-in for that principal can supply a value (for an invited member, not until they accept and sign in), and never updates again; editing the list therefore affects only profiles not yet filled.

⚠️ It takes effect when the app context is CREATED. Re-applying a blueprint against a context that already exists does not change the declaration in either direction — a declared list is not applied, and an empty list does not disable. Change an existing context's declaration through the platform API. Like issuers, it applies in the bootstrap-token phase, because setting it needs the platform provisioning capability only that phase's credential carries.

Namespaces

namespaces:
  - namespace: team
    entityBacked: true
    specificityRank: 10

A blueprint may also declare top-level namespaces — entity-namespace registrations, each { namespace, specificityRank, entityBacked?, membershipRecordType?, membershipTargetField?, membershipLevelField?, membershipLevels? }. Like issuers, these are applied in the loader's bootstrap-token phase. Every declared namespace is always owned by the blueprint's own contextId — a blueprint has no way to register a tenant-wide namespace, since it applies with a context-bound credential (the platform's namespace registration confines a context-bound caller to its own context unconditionally, even for a request that would otherwise be tenant-wide).

  • namespace — 2-32 chars, a lowercase letter first, then lowercase letters/digits/ _/-. A closed set of words is rejected as reserved (user, record, document, entity, self, tenant, context, scope, versions, lookup) — org/client are NOT in that set: they're reserved namespace names, not built-ins, registered the same way as any other (see identity-access/reference.md for the platform endpoint this mirrors). They already exist tenant-wide in every account at specificityRank 1000/2000 (below); a context-owned registration needs a different rank, and shadows the tenant-wide one for this context's own callers.
  • specificityRank — an integer 0..1_000_000, this namespace's position in the account's specificity order. Must be unique among this blueprint's own namespaces — a collision with the rest of the account's registrations (including org=1000 and client=2000) surfaces at apply time.
  • entityBacked (optional) — when true, every value in this namespace must resolve to an existing identity entity; when false/omitted, values are free-form strings validated by grammar only.
  • membershipRecordType + membershipTargetField — optional, declared together (or both omitted): which record type + field hold grants of this namespace's values. membershipRecordType must name a typeName this same blueprint declares under schemas:. Declaring this grants nobody anything on its own; a role opts in explicitly with ${{ member.scope.<namespace> }} in its dataScope.
  • membershipLevelField + membershipLevels — optional, declared together: the field naming a grant's level, and the complete set of level labels allowed.

Registration is not idempotent server-side: a re-apply whose declaration matches what's already registered converges silently, but one that disagrees with the live registration fails the apply rather than silently overwriting it.

Inputs (install-time variables)

A blueprint may declare an inputs: block and reference values with ${{ inputs.<name> }} (plus the built-ins ${{ vectros.context }} / ${{ vectros.suffix }}). Supply values at validate/plan/apply time with --set or --values. Inputs apply to file blueprints only.

Bundled blueprints

Four blueprints ship with the library:

NameWhat it provisions
task-managementStructured task tracking, shareable across sessions, agents, and users. The authoring exemplar.
agentic-sdlcA whole-SDLC system of record for an AI development team — decisions, designs, references, runbooks, and post-mortems as documents, plus controls, conventions, gotchas, and a glossary as records, cross-linked and recalled by meaning.
second-brainA personal knowledge base — capture notes, ideas, and links, then ask them anything.
clinical-intakeA clinical intake data model (synthetic/illustrative).

List them with vectros blueprint list; apply one with vectros bootstrap --blueprint <name> (which also mints a key for your MCP client) or vectros blueprint apply <name> --tenant test|live (which mints none).

Notes & limits — honest glue caveats

  • A bridge token is a human prerequisite. Applying a blueprint with bootstrap requires a bridge token from the developer portal — there is no fully unattended path that mints one for you.
  • identities: blocks are resolved at apply time. Each entry declares a kind (user, or an entity namespace — org, client, or one you registered) plus an externalId; the loader resolves it through the ordinary identity API — createUser for kind: user, createEntity({ namespace: kind, ... }) for anything else — using the bridge token's own authority, and the resolved id becomes available to the rest of the blueprint as ${{ identities.<name> }}. The CLI fails closed on a blueprint that declares an entity namespace that hasn't been registered, rather than applying it partially.
  • reference fields enforce the target at write. A blueprint can declare a typed reference between record types; by default the platform requires the referenced record to exist when the referencing record is written — so if a seed record references another, seed the target first. (To opt out for an order-independent bulk load, a schema would set capabilities.validateReferences: false — not exposed in the bundled blueprints.) There is no reverse-reference query on this surface; to ask "which records reference X", declare the reference field as an equality lookup.
  • No control-plane scopes. A blueprint cannot request keys, profiles, app contexts, users, billing, admin, or entities (creating/managing identity entities, including org/client) scopes — the CLI scope gate hard-rejects them, mints nothing, and exits non-zero. Declaring principals via identities: (above) works around this the other way — the loader resolves them itself, rather than minting an entities scope for the bootstrapped key.

Where to go next

  • cli.md — init, validate, plan, bootstrap, and blueprint-test operate on these files.
  • mcp.md — the scoped key a blueprint mints is what an MCP agent runs on.
  • sdk.md — the operations a blueprint's schemas and scopes govern.
  • The blueprint walkthroughs — narrated, end-to-end builds on top of this format.