Skip to content

How to read instance data

The DNA SDK has a unified way to read data from Instances of any kind. Follow it so your code doesn't break when kinds evolve, and so you don't need to know a kind's typed model to work with it.

The blessed query surface (pre-1.0 contract)

There is exactly ONE documented way to read manifest data (s-blessed-query-surface). Everything the docs and examples teach uses these members — and the parity fixture tests/golden-fixtures/port-surface.json (section blessed_query_surface) locks them on both sides:

Read Python TypeScript
All loaded docs (in-memory) mi.instances mi.instances
Docs of a kind [d for d in mi.instances if d.kind == "Skill"] mi.instances.filter((d) => d.kind === "Skill")
Single doc next((d for d in mi.instances if d.kind == "Skill" and d.name == "x"), None) mi.instances.find((d) => d.kind === "Skill" && d.name === "x") ?? null
Indexed / record-plane query await kernel.query(scope, kind, ...) for await (... of kernel.query(scope, kind, opts))
Aggregation await kernel.count(scope, kind, ...) await kernel.count(scope, kind, opts)
Single doc, indexed (L2-cached) await kernel.get_instance(scope, kind, name) — (use kernel.query with a filter)
Root / default agent mi.root / mi.default_agent() / mi.find_agent(name) mi.root / mi.defaultAgent() / mi.findAgent(name)
Prompt composition mi.build_prompt(...) (build_prompt_async from a loop) await mi.buildPrompt(...)
Layer overlays mi.resolve(layers) (resolve_async from a loop) mi.resolve(layers)

Rule of thumb: mi.instances for the scope you already loaded; kernel.query/kernel.count when you need push-down filtering, pagination, tenant overlays, or record-plane kinds that are not part of the loaded manifest.

Deprecated (removed in 1.0): mi.all(kind) and mi.one(kind, name) still work but warn — Python raises a DeprecationWarning, TypeScript console.warns once per process — always naming the replacement above. Don't use them in new code; don't teach them.

Python

Two coexisting forms. Pick based on whether you already hold a Instance reference.

Form 1: mi.read_spec(...) — sugar for single-field reads

Use when you only need one field and don't want to keep an Instance around:

# GOOD — one-liner facade on ManifestInstance
soul_ref = mi.read_spec("Agent", "foo", "soul")
skills = mi.read_spec_list("Agent", "foo", "skills")
description = mi.read_metadata("Agent", "foo", "description")

Raises KeyError if the (kind, name) doesn't resolve. Returns the default (or None) for missing fields.

Form 2: Instance.spec.get(...) — when you already hold a handle

Use when you need multiple fields from the same doc, or when the code already holds a Instance reference (from mi.instances or a kernel.query row):

# GOOD — kind-agnostic, works for any Instance
agent = next(d for d in mi.instances if d.kind == "Agent" and d.name == "foo")
soul_ref = agent.spec.get("soul")
skills = agent.spec.get("skills") or []
description = agent.metadata.get("description")

# AVOID — attribute form is technically supported, but hides the fact
# that 'soul' is a raw spec key (not a real Python attribute). New devs
# reading the code assume it's a typed model attribute.
soul_ref = agent.spec.soul

# AVOID — getattr() is equivalent to .get() but more verbose
soul_ref = getattr(agent.spec, "soul", None)

# FOR INTERNAL CODE ONLY — raw typed Pydantic model. Use only inside
# extensions that own the kind.
soul_ref = agent.typed.soul

Rule of thumb

  • One field, no existing handle: mi.read_spec(...)
  • Multiple fields OR existing Instance handle: doc.spec.get(...)
  • Inside the extension that owns the kind: doc.typed.field is fine

TypeScript

Same two-form model as Python.

Form 1: mi.readSpec*(...) — sugar

const soul = mi.readSpecString("Agent", "foo", "soul");
const skills = mi.readSpecStringArray("Agent", "foo", "skills");

Throws if the (kind, name) pair doesn't resolve.

Form 2: readSpec*(doc, field) — when you hold an Instance

import { readSpecString, readSpecStringArray, readSpecRecord } from "dna-sdk";

const agent = mi.instances.find((d) => d.kind === "Agent" && d.name === "foo");
if (!agent) return;
const soul = readSpecString(agent, "soul");
const skills = readSpecStringArray(agent, "skills");

// AVOID — ad-hoc casts scatter type knowledge across the codebase
const soul = agent.spec.soul as string;
const skills = agent.spec.skills as string[];

The helpers throw a clear TypeError on type mismatch, so a malformed manifest fails loud instead of silently miscasting.

When typed access IS correct

Inside the extension that defines the kind (e.g. the helix extension's own HelixKind.parse(...)), you own the typed model and should use it directly. The access rules above apply to consumer code — tools, readers, writers, renderers, examples.