Built-in AI Skills

The skill catalog an agent can call, and the guard rail each skill runs under: the model never writes code, and every skill sits behind an allowlist, permission codes, quotas, audit and tenant isolation.

For how a tool is allowed to run, read AGENT_RUNTIME.md: ai.Guard is already wired into Agent.run, skill.Tool.action defaults to .execute, and ai.AgentWorker / ai.ProposalPipeline cover background execution and staged approval.

Design rules

MechanismComponentDefault behaviour
Tenant isolationSkillContext.tenant_idEntity queries filter on the declared tenant_column; db.query is wrapped by the framework with WHERE <col> = ?, which requires BusinessSkillsConfig.db_query_tenant_column (otherwise error.TenantScopeUnavailable)
SQL safetyvalidateSqlFragment + bound parametersdb.query accepts SELECT only, rejects literals, comments and ;, and caps rows
AllowlistEntity registry / allowlistentity.* reaches registered entities only; admin skills are excluded by default
Ownershipai.freeValueResult JSON is owned by ctx.allocator; the caller deep-frees
Audit and quotaAgentAuditLog / TokenQuotaTool calls are audited and metered automatically

Action classes and the guard

Every skill declares action (guard.Action), the class axis of ai.Guard. A skill that declares nothing is .execute, the strictest class, and can never pass a policy with allow_execute = false.

ClassBuilt-in skillsWhy
readdb.query, entity.lookup, entity.list, kpi.query, report.generate, admin.config.get, admin.audit.export, list_schedulable_tasks, list_jobsReads data, changes nothing
proposenotification.send, approval.submit, approval.requestHands work to someone else (a channel or an approval chain)
executeentity.create, entity.update, command.execute, schedule_job, cancel_job, admin.cache.invalidate, admin.cache.clear, admin.config.set, admin.user.manage, admin.tenant.provisionChanges state: needs allow_execute = true and the tool name

So allow = &.{ "db.query", "kpi.query" } with allow_execute = false lets an agent read data and change nothing; read tools are unaffected by that switch. The two delegated admin skills have unknown effects and stay in execute by design. Custom skills follow the same rule: no declaration means execute.

Guard sees names, not classes. A policy whose allow list contains only execute tools still reports isInert() == false while permitting nothing. Check at startup with registry.auditPolicy(allocator, guard.permissions) or AgentSpec.auditPolicy; class_denied_tools lists tools rejected by class rather than by name. See AGENT_RUNTIME.md §1.

var guard = ai.Guard.init(.{
    .allow = &.{ "db.query", "kpi.query" }, // names the policy may call
    .allow_execute = false,                 // and no execute-class tool at all
});
var spec = ai.AgentSpec{
    .name = "analyst",
    .provider = &provider,
    .skills = &registry,
    .guard = &guard,
};

const health = try spec.auditPolicy(allocator); // null when no guard is set
defer if (health) |*h| h.deinit(allocator);
// health.class_denied_tools: listed names the class gate still refuses

Read-only data skills

zmodu.ai.business.registerBusinessSkills(registry, comptime entities) registers the data skills; SkillContext.backend_ptr carries a *data.SqlxBackend.

Multi-tenant apps must use the explicit entry point. The default leaves db_query_tenant_column as null, so any db.query with a tenant_id fails closed with error.TenantScopeUnavailable.

try ai.business.registerBusinessSkillsWith(&registry, &.{}, .{
    .db_query_tenant_column = "tenant_id", // the column name in your schema
});
SkillBehaviour
db.queryRead-only parameterized SELECT with ? placeholders and args; row cap (default 20, maximum 100); rejects non-SELECT statements and free literals. With a tenant, the framework wraps the statement as SELECT * FROM (<your SQL>) AS _zt_tenant_scope WHERE _zt_tenant_scope.<col> = ? and binds the tenant as a parameter only. Your SELECT must therefore select the tenant column, or the wrapper fails with “no such column”
entity.lookupLook up a registered entity by primary key; table names are allowlisted; tenant_column adds the tenant filter automatically
entity.listEquality filters, row cap and tenant isolation

Scheduling skills (zmodu.ai.registerScheduleSkills with *ScheduleCtx in userdata): list_schedulable_tasks, schedule_job, list_jobs, cancel_job.

Business skills

SkillBehaviourRegistration
kpi.queryQuery an application-registered metric by name (KpiCtx). A dispatch carrying tenant_id runs only KpiMetric.tenant_sql (single ? tenant bind); without a declared tenant query it refuses rather than returning an unfiltered valueai.kpi.registerKpiSkills
approval.requestSubmit an approval through the application-registered chain (subject + amount); escalations enter the human queue. Requires approval:decideai.approval_api.registerApprovalRequestSkills
notification.sendDeliver to a named channel (webhook, sink or outbox), channel allowlist enforcedai.notify.registerNotifySkills

Export the catalog with zmodu.ai.skill_export.toSkillsJson / toOpenApi (an application can expose them as HTTP endpoints), or from the CLI with zmodu ai export-skills / zmodu ai openapi.

Write actions

zmodu.ai.actions.registerWriteSkills, registerCommandSkills and registerReportSkills pass the matching context through userdata; SkillContext.permissions must carry the permission code.

SkillBehaviourPermission
entity.create / entity.updateWrites to an allowlisted entity and allowlisted columns (EntitySpec.writable); the tenant column is forced from context and the model cannot supply it; primary keys are rejectedentity:write
command.executeSubmits an application-registered command through the transactional outbox; the idempotency key is SkillContext.run_id (missing run id is rejected); returns the event idcommand:execute
report.generateRuns an application-registered aggregation report as CSV or JSON, capped at 100 rows, read-onlynone

Admin and ops skills

zmodu.ai.admin.registerAdminSkills takes *AdminCtx through userdata. Permission codes are admin:cache, admin:config, admin:audit, admin:user and admin:tenant, and every admin skill must be added to the agent allowlist explicitly.

SkillBehaviour
admin.cache.invalidate / admin.cache.clearAllowlisted caches; keys reject wildcards (*, ?), and a full clear requires all=true
admin.config.get / admin.config.setAllowlisted config; set is limited to mutable keys (ConfigStore)
admin.audit.exportQuery RunAuditStore with optional kind, tenant and limit filters
admin.user.manage / admin.tenant.provisionDelegate to application callbacks; the framework does not implement user or tenant logic

Custom skills

Business skills use the same Tool contract as built-ins: parameters map to JSON Schema, results are owned by ctx.allocator (callers use ai.freeValue), required_permission plus an allowlist narrow access, and checkDeadline provides the cooperative timeout.

Templates and selection guidance (Agent, Workflow, LLM policy, MCP bridge): AI_DEV_GUIDE.md §3–§5.

Boundaries

No shell, arbitrary URL fetching, raw SQL writes or cross-tenant access. Admin skills are reachable only after an explicit allowlist entry.