AI Agent Design — ObjectStack AI Protocol
Expert instructions for designing AI skills, tools, and knowledge sources —
and the platform agents they plug into — using the ObjectStack specification.
This skill covers the Agent → Skill → Tool three-tier architecture aligned with
Salesforce Agentforce, Microsoft Copilot Studio, and ServiceNow Now Assist
patterns.
Edition boundary ( → cloud; open = MCP-only).
The in-UI AI
runtime — the
/
agents, in-product chat, and the
routes (
) — ships in the
cloud /
Enterprise distribution, not the open framework. The agent / skill / tool
schemas in
stay open, so you author
/
as source either way (
is platform-internal) — but
they only execute in a cloud / EE host. On the
open edition there is no in-product agent: expose the
app to your own AI via
(BYO-AI) for data query, and author
metadata in
source mode with an AI coding agent (Claude Code, Cursor).
When to Use This Skill
- You need to define skills — bundles of related tools bound to the
/ surfaces.
- You are configuring tools for data queries, actions, or integrations.
- You want to index ObjectStack data as a knowledge source for RAG
retrieval.
- You are choosing and configuring LLM models (model registry).
- You need to read or review agent configuration — platform-internal;
third parties extend agents via skills, not by authoring them.
Three-Tier Architecture
Agent → Skill → Tool
│ │ │
│ │ └─ Atomic operation (query, action, flow, API call)
│ └─ Capability bundle with instructions & trigger phrases
└─ Autonomous actor with role, instructions, and guardrails
Why Three Tiers?
| Tier | Analogy | Reuse Level |
|---|
| Agent | Job role (e.g., "Help Desk Agent") | Per use-case |
| Skill | Competency (e.g., "Case Management") | Across agents |
| Tool | Specific operation (e.g., "create_record") | Across skills |
Best practice: Always model via Skills first. Direct tool assignment to
agents is supported but considered legacy. Skills provide better
discoverability, instruction scoping, and reuse.
Built-in agents: & (ADR-0063 / ADR-0064)
The runtime ships exactly two platform agents, bound by surface — the user
never picks from a roster; the surface they are in selects the agent:
- — the data product (≈ Claude Chat). Conversational read / query /
explore over records, plus running the business actions the app already
exposes. End-user audience, RLS-bounded. Canonical id ().
Cloud / Enterprise — the runtime ships in the closed cloud AI runtime
(); it is the implicit copilot for any cloud / EE app
that does not pin . (Open editions have no in-product ;
use MCP.)
- — the authoring product (≈ Claude Code). Agentic authoring of
metadata (objects, fields, views, flows) through plan → draft → verify →
publish. Builder audience, governance-gated. Canonical id . Cloud-only ·
paid — ships in the cloud AI Studio plugin; Studio pins it via .
There is
no per-turn intent classifier: a
-shaped request arriving at
is declined and redirected to the Builder, never silently re-routed into
authoring (ADR-0063 §1/§5).
Legacy names are aliases only. →
and
→
resolve through the alias table for old bookmarks
and persisted
s; they are
not vocabulary — always write
/
.
is closed to third parties (
type is
allowRuntimeCreate:false, allowOrgOverride:false
): you extend the platform
with
skills, never by authoring an agent (ADR-0063 §2).
Skill → agent affinity: the field (ADR-0063 §3)
Every skill declares which surface it binds to via
surface: 'ask' | 'build' | 'both'
(defaults to
). A skill may bind only
to an agent whose surface it matches;
binds to either. The runtime
enforces this in
at load time — an incompatible binding is a
fast load error, not a silent mis-scope. An agent's tool set is the
union of
its surface-compatible skills' tools — there is no global fall-through
(ADR-0064), so
cannot author by construction.
The built-in skills and their affinities:
| Skill | | Owns | Edition |
|---|
| | , , | OSS |
| | , , , | OSS |
| | (the business actions an object exposes) | OSS |
| + | | metadata draft / verify / publish + blueprint propose / apply | cloud only |
To grant data exploration to your own (platform-internal) agent, add
/
to its
; deactivating a skill
(
) revokes that capability for every agent that references it.
skills are inert on OSS — by design, not a bug. The open
single-env framework ships only the
agent;
/
(and any third-party
skill) are supplied by
the cloud AI Studio plugin and simply do not resolve in OSS. A
-intent
turn on OSS degrades gracefully ("authoring lives in the cloud Build assistant")
instead of dead-ending — this is intentional tiering. Do not assume authoring
tools resolve in the open framework.
: the only built-in tool that draws a chart —
it aggregates an object and emits an inline
part. Auto-registered
only when an analytics service (
) is wired;
/
return numbers, not charts.
Ops: set
AI_DAILY_USER_MESSAGES=<N>
to cap user turns per user per day
(backed by the
object; no-op if unset). Adapter health is
observable at
; invalid
settings are rejected at
save time.
Agent Configuration
Reference only — third parties do not author agents. The
type is
closed (
; ADR-0063 §2): the platform ships exactly
and
, maintained by platform / cloud plugin authors. You extend
the platform with
skills + tools (and knowledge sources) — never by
adding an agent. This section documents
for reading existing
agents and for platform-internal work.
Required Properties
| Property | Type | Description |
|---|
| | Unique agent identifier |
| string | Human-readable name |
| string | Agent's persona/role description |
| string | System prompt — detailed behavioural guidance |
Important Optional Properties
| Property | Purpose |
|---|
| Array of skill names — primary capability model |
| Direct tool references — legacy fallback |
| — the product surface this agent is (default ) |
| LLM model configuration — , , , , |
| REMOVED in protocol 17 (#3896 close-out) — declaring sources/indexes on an agent never scoped retrieval ( takes from the LLM's tool-call arguments). Restrict at the knowledge-service/source level; describe intended grounding in |
| , , |
| Output format (JSON schema, regex, etc.) |
| Autonomous reasoning — (default 10) |
| persistence + |
| Permission-set capabilities required to use the agent |
| Enable/disable the agent |
There is
no top-level / on an agent — sampling
parameters live under
(
).
Agent Example
<!-- os:check -->
typescript
import { defineAgent } from '@objectstack/spec';
export default defineAgent({
name: 'support_tier_1',
label: 'First Line Support',
role: 'Help Desk Assistant for customer support cases',
instructions: `
You are a friendly and professional help desk assistant.
RULES:
- Always greet the customer by name if available.
- Search the knowledge base before creating a new case.
- Escalate to a human agent if the issue is critical or security-related.
- Never share internal system details with customers.
- Respond in the customer's preferred language.
`,
skills: ['case_management', 'knowledge_search'],
model: {
provider: 'openai',
model: 'gpt-4o',
temperature: 0.3,
},
guardrails: {
blockedTopics: ['internal_pricing', 'employee_data'], // forbidden topics / action names
maxTokensPerInvocation: 8000, // token budget per invocation
maxExecutionTimeSec: 60, // wall-clock cap per invocation
},
});
Skill Configuration
A Skill is a named bundle of tools with dedicated instructions and
trigger conditions.
Required Properties
| Property | Type | Description |
|---|
| | Unique skill identifier () |
| string | Human-readable name |
| | Tool names this skill grants access to (trailing wildcard allowed, e.g. ) |
Important Optional Properties
| Property | Purpose |
|---|
| — agent surface affinity (default ; see above) |
| What the skill does — helps the agent decide when to use it |
| LLM prompt guidance specific to this skill's context |
| Natural language phrases that activate the skill |
| Programmatic activation rules |
| Is the skill enabled (default: ) |
A skill has
no key — it was removed in 16.x. Skill invocation
was never gated by it (the registry reads only
/
/
), and a security-shaped field that enforces nothing is worse than no
field at all. Gate access at the
agent instead —
/
on
are enforced at the chat route — or on the underlying actions the
skill's tools call (permission sets, ADR-0066).
Skill Example
<!-- os:check -->
typescript
import { defineSkill } from '@objectstack/spec';
export default defineSkill({
name: 'case_management',
label: 'Case Management',
description: 'Create, update, query, and escalate support cases.',
instructions: `
When managing cases:
- Always check for duplicate cases before creating a new one.
- Set priority based on customer tier: Enterprise → High, Pro → Medium, Free → Low.
- Escalated cases must include a summary of actions already taken.
`,
tools: [
'query_support_case',
'create_support_case',
'update_support_case',
'escalate_case',
],
triggerConditions: [
{ field: 'objectName', operator: 'eq', value: 'support_case' },
],
active: true,
});
Trigger Conditions
| Operator | Meaning |
|---|
| Equals |
| Not equals |
| Value is in array |
| Value is not in array |
| String contains substring |
Tool Configuration
Tools are the atomic operations that skills expose to agents.
First-Class Tool Metadata ()
A tool authored as metadata (
,
) is validated by
: required
/
/
, a
JSON Schema
object, plus optional
and
.
is
strict — an unknown key (a typo, or a retired key) is a parse error, not
a silent strip. Retired in the #3896 close-out:
,
,
and
(all were authorable and inert;
gated
nothing and
withdrew nothing — the rejection message carries
each key's replacement), joining
(#3715).
<!-- os:check -->
typescript
import { defineTool } from '@objectstack/spec';
export default defineTool({
name: 'create_case',
label: 'Create Support Case',
description: 'Creates a new support case record',
parameters: {
type: 'object',
properties: {
subject: { type: 'string', description: 'Case subject' },
priority: { type: 'string', enum: ['low', 'medium', 'high'] },
},
required: ['subject'],
},
objectName: 'support_case',
});
To gate what a tool can do, gate the underlying action
(
action.requiredPermissions
, ADR-0066) or the objects it touches; to withdraw
a tool, remove it from the skills/agents that reference it. Categorization, if
you need it, belongs on the action side (
— a live,
enforced surface).
Tool metadata is a read-only projection — not an execution entry point.
has no
/
field, and no framework
executor loads a metadata-authored tool. The runtime executes a
separately-registered
(cloud
);
tool metadata is a one-way projection for Studio / discovery. Do not expect a
hand-authored tool to run in the open edition (liveness audit #1878/#1892).
Inline Agent (legacy)
Entries in an agent's inline
array are a
different, legacy shape
(
):
{ type: 'action' | 'flow' | 'query' | 'vector_search', name, description? }
— references to existing actions / flows / queries, not
tool definitions. Prefer skills + first-class tool names.
Auto-Exposed Actions
Cloud / EE runtime. ,
, and
the HITL approval queue below ship in
— the closed
cloud / Enterprise runtime, not an open package. On the open edition, expose
actions to your own AI via
instead.
You usually
don't author tool definitions by hand for action invocation. Every
you attach to an object via
defineObject({ actions: [...] })
is auto-exposed as a tool named
by
(invoked from
).
Three action types dispatch headlessly:
| Dispatch | Wiring |
|---|
| IDataEngine.executeAction(object, target, ctx)
— same as Studio's row toolbar | none |
| HTTP call to (-based by default) | AIServicePlugin({ apiActionBaseUrl, apiActionHeaders })
or custom |
| IAutomationService.execute(target, { triggerData })
| service registered with the kernel |
Skipped automatically:
- UI-only types (, , ).
- Dangerous variants ( set, , ) — unless the plugin is started with
enableActionApproval: true
, in which case they route through the HITL approval queue (see below).
- Owner opt-outs ().
body assembly (last wins): user params →
(using
, default
) →
.
bodyShape: { wrap: 'data' }
nests user params under
while keeping
flat.
Use
actionSkipReason(action, ctx)
(exported from
— cloud-only, not importable on the open edition) when authoring an action and you want to know
why it isn't surfacing in chat. Studio's "AI exposure" diagnostics use the same predicate. Pair with
actionRequiresApproval(action)
to know whether a registered action will be routed through HITL.
Human-In-The-Loop approval
Cloud / EE runtime. The HITL approval queue is part of
and is not available in the open framework.
ts
kernel.use(new AIServicePlugin({
enableActionApproval: true, // opt in; default is false
apiActionBaseUrl: process.env.OS_AI_ACTION_API_BASE_URL,
}));
Flow:
- LLM picks → runtime persists an row and returns
{ status: 'pending_approval', pendingActionId }
.
- Operator triages via Studio's AI Pending Actions inbox (or the REST endpoints:
GET/POST /api/v1/ai/pending-actions/...
).
- Approve → service re-runs the action via the pre-registered bypass-approval dispatcher; row transitions to / .
- Reject → row transitions to with an optional reason.
Programmatic API on
:
,
,
,
. All are optional (returns clear error when no
is wired).
Knowledge Sources (RAG)
The platform's RAG primitive is the
KnowledgeSource
(
in
): declarative metadata
pairing
what to index with the id of an
that does the
work. Sources are registered at runtime via
IKnowledgeService.registerSource()
(there is no
collection for
them), and the
tool exposes registered sources to agents.
KnowledgeSource Structure
| Property | Purpose |
|---|
| Snake_case source id |
| / | Display metadata |
| Adapter id (e.g. , ), resolved via IKnowledgeService.registerAdapter
|
| Adapter-specific configuration (opaque to the service) |
| What gets indexed — discriminated on : | | |
| Optional ref (, , ) — adapters that manage embeddings internally (RAGFlow, Dify, Vectara) may ignore it |
| Optional ref (, ) — same caveat |
| (default for object sources) + optional (surfaced for an external scheduler, not self-scheduled) |
| Whether may expose this source to agents (default ) |
Source kinds:
| Fields |
|---|
| , (min 1; = every readable text field), , (ObjectQL syntax) |
| (storage prefix, e.g. ), |
| , |
Knowledge Source Example
<!-- os:check -->
typescript
import { KnowledgeSourceSchema, type KnowledgeSource } from '@objectstack/spec/ai';
export const supportKb: KnowledgeSource = KnowledgeSourceSchema.parse({
id: 'support_kb',
label: 'Support Knowledge Base',
adapter: 'ragflow', // or 'memory' for dev/test
source: {
kind: 'object',
object: 'kb_article',
contentFields: ['title', 'body'], // concatenated into document content
metadataFields: ['category', 'owner_id'], // projected for search-time filtering
where: { published: true }, // index published articles only
},
refresh: { onRecordChange: true }, // re-index on record.* events
});
Chunking, top-K, score thresholds, and rerankers are NOT platform
metadata. The spec deliberately scopes them out (
):
chunking strategies, retrieval pipelines, and RAG orchestration belong to
the adapter (
) or application code. The platform only carries
the embed + vector primitives so any RAG strategy can be built on top.
Knowledge Source Best Practices
- Filter with . Index only published/active records
(
where: { published: true }
) so draft or archived content never enters
the index.
- Index only meaningful text via . Do not include system
fields or IDs; use (all readable text fields) sparingly.
- Project filter fields into (e.g. , ,
) so searches can be narrowed at query time.
- Hide with when a source should be indexed but not
agent-searchable.
- Tune relevance in the adapter, not the metadata. Top-K, thresholds, and
reranking are configured in your RAG backend (via ), not in
ObjectStack metadata.
Model Configuration
Supported Providers
| Provider | Models | Use Case |
|---|
| GPT-4o, GPT-4o-mini, o1, o3-mini | General purpose, reasoning |
| Claude Sonnet 4, Claude Haiku | Long context, safety |
| Same as OpenAI, enterprise managed | Compliance, data residency |
| Ollama, vLLM, llama.cpp | On-premise, air-gapped |
The inline agent
enum is the narrow set above
(
/
/
/
).
Model-registry entries
(
) accept a wider set: also
,
,
,
.
Model Selection Guidelines
| Scenario | Recommended |
|---|
| Complex reasoning, multi-step planning | GPT-4o / Claude Sonnet 4 |
| High-volume, low-latency | GPT-4o-mini / Claude Haiku |
| Sensitive data, on-premise | Local models via Ollama |
| Structured data extraction | Any model + config |
Temperature Guidelines
| Value | Use Case |
|---|
| Factual Q&A, data extraction, code generation |
| Conversational agents, customer support |
| Creative writing, brainstorming |
| Experimental / highly creative (use with caution) |
Structured Output
Force the agent to respond in a specific format:
typescript
structuredOutput: {
format: 'json_schema',
schema: {
type: 'object',
properties: {
summary: { type: 'string' },
priority: { type: 'string', enum: ['low', 'medium', 'high'] },
action_items: { type: 'array', items: { type: 'string' } },
},
required: ['summary', 'priority'],
},
strict: true, // enforce exact schema compliance (default: false)
maxRetries: 3, // max retries on validation failure (default: 3)
}
On validation failure the runtime retries by default
(
retryOnValidationFailure: true
). Optional extras:
and a
of post-processing steps (
,
,
,
). There is no
object — the knobs are
+
.
Common Pitfalls
- Overly broad instructions. Agents with vague instructions hallucinate
more. Be specific about what the agent should and should not do.
- Too many tools per skill. Keep skills focused (3–8 tools). If a skill
has 15+ tools, split it.
- Missing guardrails and approval gates. Define (plus the
token / time budgets) in agent ; for destructive operations put
a human in the loop with a gate that is actually enforced —
enableActionApproval: true
(HITL queue, cloud) for auto-exposed actions,
on the action, or on an
MCP tool binding. AI metadata edits are already gated: they land as drafts a
human must publish (ADR-0033).
⚠️ on the tool was REMOVED (#3715, ADR-0033 §2) —
it was read by no execution path, so it produced no pause. is
strict, so authoring it now fails the parse with the migration attached.
There is no field.
- Ignoring tool descriptions. The LLM uses tool to decide
when to call it. Poor descriptions = wrong tool selection.
- Not testing trigger phrases. Ambiguous trigger phrases cause skill
conflicts. Test with edge-case inputs.
- Indexing everything. A knowledge source without a filter and
curated fills the index with drafts and boilerplate that
pollute retrieval. Source hygiene is the metadata's job; relevance tuning
(top-K, thresholds, reranking) belongs to the adapter.
App AI Blueprint (Skills + Tools + Knowledge)
Reference layout for a scaffolded app:
| Layer | File | Pattern |
|---|
| Reusable skill | src/skills/lead-qualification.skill.ts
| — trigger phrases + trigger conditions + bounded toolset; pick a |
| Tool metadata | src/tools/query-leads.tool.ts
| — JSON-Schema ; a discovery projection, not an executor (see caveat above) |
| Knowledge source | src/knowledge/sales-kb.ts
| metadata, registered at runtime via IKnowledgeService.registerSource()
|
| Central registration | defineStack({ skills: [...], tools: [...] })
| / / are the only AI stack collections — knowledge sources have none; agents are platform-supplied |
Default for metadata apps: push business capability logic into skills, keep
tools atomic, and wire domain knowledge through knowledge sources.
Verify your work
After authoring a
/
(or platform-internal
) or a model-registry entry, run the author-time gate before
reporting done:
bash
os validate # Zod schema + CEL predicate validation + bindings (no artifact)
# or: os build # the same gates, plus emits dist/
It confirms the agent/tool/model metadata conforms to the protocol and that any
CEL predicate (e.g. a tool's availability condition) parses and resolves. In a
scaffolded project the gate is
. See objectstack-platform →
Verify your work.
References
See
references/_index.md for the full list of Zod
schemas (with one-line descriptions) — pointers into
node_modules/@objectstack/spec/src/
. Always
the source for exact field
shapes; do not rely on memory of property names.