Reference

Composition

The ingredients and operations tables inside a pack — execution bindings, risk and approval, arguments, connections, and what publishing does and does not check.

A composition is the capability half of a pack: one surface Recued can talk to, and the operations a recipe may call against it. The Pack editor produces one for you, and most authors never read this page. It is here because you may want to generate a pack — from an API description, from a script, or from a tool of your own — and because publishing an artifact you did not hand-write is a supported path.

Everything below is what publish and install actually validate.

#Where it sits

json
{
  "manifest_version": 2,
  "artifact_type": "pack",
  "pack_kind": "app_pack",
  "slug": "acme-tasks",
  "publisher": "your-publisher-handle",
  "name": "Acme Tasks",
  "description": "Read and act on Acme tasks.",
  "version": 1,
  "recipes": [],
  "tags": ["pack:acme"],
  "contents": [{ "type": "composition", "composition": { } }]
}

publisher must equal the handle you are authenticated as when you publish. See the pack manifest for the rest of the envelope.

#The composition

json
{
  "schema_version": 1,
  "slug": "acme-catalog",
  "catalog_kind": "official",
  "ingredients": [],
  "operations": []
}
Field Notes
schema_version 1
slug Required — the catalog identity
catalog_kind official, unofficial_acknowledged, or private_byo — how authoritative your description of the vendor is
ingredients Exactly one row. See below
operations At least one row

One composition describes one surface, so ingredients carries a single entry. A vendor with two incompatible API versions is two packs, and the second one depends on the first rather than adding a second ingredient.

#Ingredients — what you can talk to

json
{
  "slug": "acme-catalog",
  "kind": "http",
  "http": {
    "base": "https://api.acme.example",
    "connection": "acme",
    "result_path": "items"
  },
  "entities": {}
}
Field Notes
slug Joins to each operation's ingredient
kind http, cli, connection, mcp, or storage
http.base Origin only. Put the full path in each operation's path_template
http.connection Which connection record supplies credentials
http.result_path Surface-wide default for where records live in a response; an operation may override it
entities Optional vendor entity schemas, if your pack projects records into a canonical shape

Keep base origin-only. A base carrying a version prefix plus an operation path that starts with / drops the prefix, because that is what URL resolution does. Both forms compose correctly today, but origin-only is the convention and it is the one that never surprises anyone.

A kind: "storage" ingredient is a pack with its own local rows rather than a remote API; see Authoring ingredient packs for what that substrate can and cannot do.

#Operations — what a recipe can call

json
[
  {
    "op": "task.list",
    "ingredient": "acme-catalog",
    "risk": "read",
    "approval": "never",
    "idempotency": "safe",
    "description": "List my open Acme tasks.",
    "args": [
      { "key": "query.assignee", "type": "string" }
    ],
    "bind": {
      "kind": "rest",
      "method": "GET",
      "path_template": "/v1/tasks",
      "static_query": { "limit": "100" },
      "static_headers": { "Accept": "application/json" }
    },
    "result_path": "items",
    "cache_ttl_ms": 60000
  },
  {
    "op": "task.complete",
    "ingredient": "acme-catalog",
    "risk": "write",
    "approval": "ask",
    "idempotency": "idempotent",
    "description": "Mark one Acme task complete.",
    "args": [
      { "key": "task_id", "type": "string", "required": true, "affects_target": true },
      { "key": "body.note", "type": "string" }
    ],
    "bind": {
      "kind": "rest",
      "method": "POST",
      "path_template": "/v1/tasks/{{task_id}}/complete"
    }
  }
]

The read takes a filter and no target; the write takes the identity of the one record it changes, and says so.

Field Notes
op The name a recipe calls, as publisher.pack.op once installed
ingredient The ingredient slug above
risk read, write, admin, or destructive
approval never, ask, or always
idempotency safe, idempotent, or non_idempotent
description What a model reads when deciding whether to pick this
args The public input surface — see below
bind The execution binding — see below
result_path Where records live in this operation's response. Per-operation wins over the surface default
pagination Optional paging declaration
cache_ttl_ms Optional response cache

🔑 risk and approval are the real security surface. What an operation may do is governed by the grant it runs under and the approval gate — never by how well documented the vendor is, and never by how the operation is named. Declare the posture the behaviour actually has: an operation that changes someone else's state is not read because it happens to be a GET.

#Arguments

Field Notes
key The argument name a recipe passes. Dotted prefixes place it: query.*, body.*, header.*, or a bare name for a path variable
type string, number, boolean, object, array
required Whether the call is invalid without it
affects_target Marks the argument that identifies which record is acted on. Governance reads this to describe the blast radius at the approval gate

A path variable's name is yours, not the vendor's — it is the {{task_id}} in your own path_template, not the parameter name in their API description.

#Bindings

bind.kind Carries
rest method, path_template, optional static_query, static_headers, static_body
graphql The document and variable mapping
cli_invocation An argument vector for a local command — never an interpolated shell string
core.records An action and entity against the pack's own local rows

static_query is the sparse-fieldset lever. If your vendor only returns fields you ask for, every field path your pack later reads has to be requested here. Otherwise it resolves to nothing at run time, quietly, and you will read that as "the vendor returned nothing" rather than "I never asked."

For an API that can return integers beyond JavaScript's exact range, declare bind.response_json: { "unsafe_integers": "string" } so an identity is never silently rounded; oversized integers then arrive as exact decimal strings while everything else keeps its JSON type.

#Connections

An ingredient names a connection. That record holds the credential AEAD-encrypted on the user's own server — never in your pack, never in Recued's cloud, never in the manifest you publish.

  • Riding an existing vendor — name it by slug and the user picks an enrolled connection at install.
  • Bringing a new vendor — the user enrols it through the generic connection form: base URL and auth. Recued's setup guide fills that gap: it points at the provider's developer page, identifies the auth type, and suggests values for the individual fields, so an author bringing an unknown vendor is not handing users a blank form.

#Pre-filling the form

You know your vendor's base URL and where its tokens are issued. Say so, and the form opens with those boxes already filled:

json
{
  "connection_hints": [
    {
      "connection": "acme",
      "values": {
        "config.base_url": "https://api.acme.example",
        "auth.scope": "tasks.read tasks.write"
      },
      "setup_url": "https://acme.example/developers/tokens"
    }
  ]
}

connection_hints sits beside contents on the manifest and is open to every publisher. connection names a connection one of your ingredients uses.

A hint sets a value, and only on a field the user can see and edit. It cannot hide a field, lock one, or reveal one — those belong to the connection form itself, not to your pack — so a value you get wrong is something the user reads and corrects rather than something that happens to them. A pre-filled box is labelled with your publisher handle until the user edits it.

Field key Pre-fills
config.base_url The API endpoint
subresource_path The sub-resource permission boundary
auth.scope, auth.scopes Requested scopes
auth.token_endpoint, auth.authorize_url OAuth endpoints
auth.param_name The query-parameter name for key auth

A key on that list can still do nothing, silently. Publishing checks the key is spellable, not that the connection you are hinting actually shows it. A hint is dropped when the form has no such field, and when the form pins the field (a registered vendor may fix its own endpoint, and a hint may not move it). The OAuth keys — auth.scope, auth.scopes, auth.token_endpoint, auth.authorize_url — only appear once the user picks a matching OAuth auth type, so hinting them does nothing for a user who signs in with an API key. Nothing warns you, at publish or at install: hint the keys your connection genuinely uses, and check the form once with a real install.

Nothing else is accepted, and a key outside that list fails your publish rather than being quietly dropped. Endpoint values must be a public https URL carrying no credentials — http, localhost, a private address, or a user:password@ prefix is refused, and so is a value over 500 characters. setup_url is held to the same bar, because it is a link you are inviting someone to follow.

There is no credential here and there cannot be. A hint pre-fills the boxes around a secret; the secret itself is always something the user supplies.

At most eight hint entries per pack. An entry carries connection, values and setup_url and nothing else — the connection_requirements cells (api_base, vendor, authority, identity_endpoint, auth, per_org) are refused inside a hint rather than ignored, so a declaration you thought was accepted never silently does nothing.

When the user sees it. The pre-fill lands after your pack is installed, not on the install screen: install → the pack's Connections row → Set up → → the form, filled and labelled with your handle. The install screen names the connection your pack wants, so the user knows that before granting anything, but it does not open the form — nothing is installed yet at that point, so there is no pack to read hints from. Write your setup instructions in that order.

#What third-party publishers can and cannot do

You can author a full composition, bring a vendor Recued has never heard of, ship without any API description, declare synced Sources, pre-fill the connection form with connection_hints, and publish and install. This path is exercised end to end.

You cannot declare connection_requirements. That descriptor is not a richer hint — it decides which of the user's existing connections your pack is offered for adoption, tags the connection as a particular vendor, names an OAuth issuer, and supplies the endpoint used to deduplicate accounts. Each of those acts without the user reading a form, which is why it is reserved to Recued's own publisher and wired into first-party setup code.

The practical difference is one click. A first-party pack can offer Connect account on the install screen; yours asks the user to enrol through the connection form. With hints and the setup guide that form arrives mostly filled in, but it is still a form, and it is honest to say so rather than imply parity.

You cannot write trust_summary. Verification is computed by Recued, not asserted by the publisher — a self-published pack cannot mark itself verified, which is the point.

⏭ These two are related, and the direction is worth knowing while you build: publisher verification is what a streamlined enrolment should eventually be earned by, rather than being permanently first-party. That work is not built and this page will change when it is.

#What publishing checks, and what it does not

Publish Install
Composition shape — missing tables, unknown kind, an operation naming no ingredient rejected rejected
An API description the catalog does not pin rejected rejected
Shipping no API description at all fine fine
A field path naming something the API never returns not checked not checked

That last row is deliberate and worth understanding. Nothing machine-checks a field path: proving an operation proves its method and route and nothing about the fields inside a response. A gate built on it would go green while proving nothing that matters, and "it passed" would then read as "it was verified" — a check that looks like assurance and is not is worse than no check at all.

The answer is visibility instead: the runtime reports a declared field that has never carried a value on any record, which is exactly what a wrong path looks like, and it treats first- and third-party packs identically.

Your own declaration is the control, not the gate. Prove your paths against the operation as you shipped it, not against a convenient hand-made request.

#Two failure modes worth naming

A never-populated field may be a wrong path — or your own filter. Before blaming the declaration, re-read the list operation that feeds it. A field projected from a closed record is structurally dead if the query excludes closed records: the path was right and the scope was wrong.

A list that returns identity only must be hydrated. If the list gives back { id, url } and every real field arrives on the individual read, then measuring the list alone reports every field as never-seen. That is a shape artefact, not a broken declaration.

Recued is local first; your server remains the authority.

Recued Docs

Search documentation

Start typing to search the documentation.