Install
$ agentstack add skill-zenstackhq-skills-zenstack-plugin-dev ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
ZenStack V3 — Plugin Development
> Plugins are a preview feature and may change in future releases.
A ZenStack plugin can extend the toolkit at three levels, independently or together:
- Schema (ZModel) — contribute custom attributes (e.g.
@password) and functions to the
schema language.
- CLI (code generation) — participate in
zen generateto emit artifacts from the schema. - Runtime (ORM) — install on the client to intercept queries, hook CRUD lifecycle events,
transform SQL, add custom methods, or compute fields.
ZenStack's plugin replaces Prisma's generator. To declare/consume plugins in a schema see zenstack-schema-modeling; this skill is about authoring them.
Packages: @zenstackhq/orm (runtime plugin types), @zenstackhq/sdk (CLI plugin types + ZModel AST). Install the SDK as a dev dependency: npm install -D @zenstackhq/sdk.
Declaring a plugin in ZModel
plugin myPlugin {
provider = './my-plugin' // local .ts/.js/.mts/.mjs file, a folder with an index, or an npm package
output = './generated' // arbitrary key=value options, readable by the plugin
}
provider resolution order: local file path → folder with an index → npm package. CLI plugins must be ESM; TypeScript providers are loaded via jiti. The built-in @core/typescript plugin runs automatically; @core/prisma emits a Prisma schema.
Runtime plugins
A runtime plugin is an object implementing RuntimePlugin from @zenstackhq/orm. Define it three ways; definePlugin gives the best type inference:
import { definePlugin, type RuntimePlugin } from '@zenstackhq/orm';
// plain object / definePlugin (recommended)
const plugin = definePlugin({ id: 'my-plugin', /* hooks… */ });
// or a class
class MyPlugin implements RuntimePlugin {
id = 'my-plugin';
/* hooks… */
}
Install on the client (immutable — returns a new client, original unchanged); remove with $unuse(id) / $unuseAll(). You can also pass plugins: [...] to the ZenStackClient constructor.
const db = new ZenStackClient(schema, { dialect });
const withPlugin = db.$use(plugin); // new client
const plain = withPlugin.$unuse('my-plugin'); // remove by id
// or: new ZenStackClient(schema, { dialect, plugins: [plugin] });
Every plugin needs a unique id. Add any combination of the hooks below.
onQuery — intercept ORM operations
Fires for ORM API calls only (findMany, create, …), not the query builder. Wrap, modify args, short-circuit, or post-process results via proceed.
definePlugin({
id: 'timing',
onQuery: async ({ model, operation, args, proceed }) => {
const start = Date.now();
const result = await proceed(args);
console.log(`${model}.${operation} took ${Date.now() - start}ms`);
return result;
},
});
onKyselyQuery — intercept/transform SQL
The lowest-level hook: every DB access (ORM and query builder) flows through Kysely here, so you can inspect or rewrite the SQL AST (OperationNode) before proceed. Good for soft-deletes, injected filters, column transforms.
import type { OnKyselyQueryCallback } from '@zenstackhq/orm';
const hook: OnKyselyQueryCallback = async ({ query, proceed }) => {
// inspect/transform `query` (a Kysely OperationNode), e.g. with an OperationNodeTransformer
return proceed(query);
};
definePlugin({ id: 'sql-rewrite', onKyselyQuery: hook });
onEntityMutation — before/after create·update·delete
Lifecycle hooks around entity mutations, with lazy loaders for the affected rows.
definePlugin({
id: 'audit',
onEntityMutation: {
runAfterMutationWithinTransaction: false, // default: run after the tx commits
async beforeEntityMutation({ model, action, loadBeforeMutationEntities }) {
// action: 'create' | 'update' | 'delete'
if (model === 'Post' && action === 'update') {
const before = await loadBeforeMutationEntities();
}
},
async afterEntityMutation({ model, action, loadAfterMutationEntities }) {
const after = await loadAfterMutationEntities();
},
},
});
Set runAfterMutationWithinTransaction: true to run the after-hook atomically inside the mutation's transaction. Note: mutations triggered by DB cascades are not captured, and the loaders can be expensive on bulk mutations.
Extending the client (client)
Add custom methods/properties under client. Names must be $-prefixed so they don't shadow model accessors; grouping under one $namespace object is recommended.
const cacheDb = db.$use(definePlugin({
id: 'cache',
client: {
$cache: {
get stats() { return { hits: 0, misses: 0 }; },
async invalidate() { /* … */ },
},
},
}));
await cacheDb.$cache.invalidate();
Custom query args (queryArgs)
Accept extra options on ORM calls, validated with a Zod schema. Target $all, the $create/$read/$update/$delete groups, or specific operations.
import { z } from 'zod';
definePlugin({
id: 'cache',
queryArgs: {
$read: z.object({ cache: z.strictObject({ ttl: z.number().positive().optional() }).optional() }),
},
onQuery: ({ args, proceed }) => proceed(args), // read (args as any).cache
});
// await db.user.findMany({ where: {...}, cache: { ttl: 60 } });
Computed result fields (result, v3.5.0+)
Add virtual fields computed after a query from declared needs fields.
definePlugin({
id: 'full-name',
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute: (user) => `${user.firstName} ${user.lastName}`,
},
},
},
});
CLI plugins (code generation)
A CLI plugin is an ESM module whose default export is a CliPlugin with a generate callback. It runs during zen generate, receiving the parsed ZModel AST.
import type { CliPlugin } from '@zenstackhq/sdk';
import { isDataModel, type InvocationExpr } from '@zenstackhq/sdk/ast';
import fs from 'node:fs';
const plugin: CliPlugin = {
name: 'Password Report',
// statusText is optional
generate: ({ model, defaultOutputPath, pluginOptions }) => {
if (pluginOptions['report'] !== true) return; // options come from the ZModel plugin block
let out = '# Password Fields\n\n';
for (const dm of model.declarations.filter(isDataModel)) {
for (const field of dm.fields) {
const attr = field.attributes.find((a) => a.decl.$refText === '@password');
if (attr) out += `- ${dm.name}.${field.name}\n`;
}
}
fs.writeFileSync(`${defaultOutputPath}/password-fields.md`, out, 'utf-8');
},
};
export default plugin;
generate receives { model, defaultOutputPath, pluginOptions } — the ZModel AST, the resolved output directory (the output option or a default), and the options from the plugin block.
Schema plugins (custom attributes & functions)
Declare new ZModel attributes/functions in a .zmodel the plugin ships, then use them in the app schema. @@@targetField constrains where an attribute applies.
// my-plugin/plugin.zmodel
attribute @password(hasher: Any) @@@targetField([StringField])
function bcryptHasher(rounds: Int): Any {}
// app schema
model User {
id Int @id @default(autoincrement())
password String @password(hasher: bcryptHasher(10))
}
End-to-end pattern: password hasher
The docs' worked example combines all three levels into one plugin package:
- Schema —
plugin.zmodeldefines@password+bcryptHasher(); the app schema applies
@password(hasher: bcryptHasher(10)) to a field.
- CLI — a
CliPlugin.generatewalks the AST and emits a "password fields" report when the
report option is set.
- Runtime — a
RuntimePluginwithonKyselyQueryuses a KyselyOperationNodeTransformerto
detect @password fields (via schema.models[...].fields[...].attributes) and hash values on INSERT/UPDATE before they hit the database.
Install with db.$use(new PasswordHasherPlugin()) (or the plugins constructor option), then npm install -D @zenstackhq/sdk and zen generate to run the CLI side.
> Use Zod v4 for queryArgs schemas to match ZenStack's bundled version.
Reference docs
Full ZenStack documentation for this topic is bundled under [references/](references/):
- [plugin-dev.md](references/plugin-dev.md) — end-to-end plugin development guide
- [runtime-plugins-overview.md](references/runtime-plugins-overview.md) — runtime plugin model
- [extending-orm-client.md](references/extending-orm-client.md) — client methods, query args, computed fields
- [entity-mutation-hooks.md](references/entity-mutation-hooks.md) — before/after mutation hooks
- [kysely-query-hooks.md](references/kysely-query-hooks.md) —
onKyselyQuerySQL transformation - [query-api-hooks.md](references/query-api-hooks.md) —
onQueryORM interception - [zmodel-plugin.md](references/zmodel-plugin.md) — declaring plugins in ZModel
- [zmodel-plugin-reference.md](references/zmodel-plugin-reference.md) —
pluginblock reference
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: zenstackhq
- Source: zenstackhq/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.