Clawnify
Clawnify v1.0.3
Knowledge, agents and apps
From the developer
Declared capabilities
- Search company knowledge
- Review AI agent sessions
- Manage apps and their data
Cost & access
Clawnify does not sell products or take payments in ChatGPT or Codex.
Declared limitations
- Bulk deletion without naming what to delete
- Request unrelated to Clawnify
- Data from an organization the account does not belong to
Onboarding example
What does our company knowledge say about our refund policy?
Sources & dates
Developer declarations; not independently tested. Listing last checked Nov 5, 2026. Package observed Nov 5, 2026.
Description
From the marketplace listing
Clawnify runs AI agents, business apps and a company knowledge base for your organization. Connect your Clawnify account to search and cite your company's policies and documents, see what your AI agents did and adjust their instructions, look up your apps and flows and work with their data, and keep shared memory and tasks. The included skills cover building Clawnify apps and websites and organizing company knowledge. Requires a Clawnify account. It only reaches the organizations the signed-in person belongs to.
Language: English · Automatically detected from descriptions.
Changes
No changes recorded yet. Follow this plugin to keep track of future changes.
First seen Nov 5, 2026. Last seen Nov 5, 2026.
Similar plugins
Find competitors to follow as you develop your plugin.
-
Trove gives ChatGPT a library it can read. Search and read the PDFs, ebooks and markdown documents in your Trove account, answer with pag...
Shared terms: knowledge, agent, cite, document
-
Connect ChatGPT to your organization's Sensei knowledge base. Find grounded answers with source articles, explore related policies, prepa...
Shared terms: knowledge, base, organization, policie
-
Search all your company's knowledge from one place with Kapa, and put that same knowledge to work in your own agents and on your website....
Shared terms: knowledge, base, company, website
-
GoSearch connects your AI assistant to your organization's knowledge base. Ask a question and get answers pulled from the apps your team ...
Shared terms: knowledge, base, organization, company
-
Bring enterprise context to ChatGPT and your AI tools with Glean's Remote MCP server. Search across all your organization's connected dat...
Shared terms: knowledge, memory, organization, company
Keywords
Files & skills
File archives
Skill instructions
build-a-clawnify-app35.5 KB
---
name: build-a-clawnify-app
description: Author a Clawnify app end-to-end — the canonical Hono + React + Vite + @hono/zod-openapi + @clawnify/db (Drizzle) stack, schema.ts as the single schema source of truth (DDL generated via pnpm db:generate), OpenAPIHono routes (createRoute) that auto-generate an OpenAPI spec, and platform-injected identity via user()/orgId()/caller(). Use when building or modifying an app that deploys to the Clawnify platform.
---
# Build a Clawnify app
Canonical skill: how to author a Clawnify app end-to-end. Same content
for humans reading the docs and for AI coding agents injecting this as
instructions during an app build.
If you're an agent: this document is enough context to produce a
working app from scratch. Follow it literally. The patterns here
are exactly what `clawnify init` scaffolds and what the platform
expects.
## TL;DR
- **One stack.** Hono + React + Vite + `@hono/zod-openapi` +
`@clawnify/db` (Drizzle) + `@clawnify/app` (entry, routing,
identity) + Tailwind v4. Nothing else.
- **Schema lives in `schema.ts` alone** — the Drizzle DSL your queries
compile against. The DDL is generated from it (`pnpm db:generate` →
`.clawnify/drizzle/*.sql`); never hand-write a `schema.sql` in a
scaffolded app.
- **Queries through `getDB`** — `import { getDB, eq } from
"@clawnify/db"`. JSON columns auto-serialize.
- **API routes are `OpenAPIHono` + `createRoute`** — Zod-validated
request/response.
- **`createApp()`** (from `@clawnify/app`) builds the entry — it returns an
`OpenAPIHono` with API discovery already mounted (`/api/openapi.json` +
`/llms.txt`, generated from your live routes). That descriptor *is* the
tool surface agents discover. Never hand-write those routes.
- **One import.** `@clawnify/app` re-exports `createRoute`, `z` and
`OpenAPIHono`, and adds `user` / `orgId` / `caller`. Import from it, not
from `@clawnify/routes` directly.
- **`clawnify.json`** declares framework, env, and public routes.
There is no `api.tools[]` — the agent discovers and calls your
endpoints via `/llms.txt` (and the OpenAPI spec).
- **Identity is injected by the platform** — read it with `user(c)` /
`orgId(c)` / `caller(c)` from `@clawnify/app`. Your app never
authenticates anyone.
## Stack rationale
One template, one set of decisions. Don't deviate unless the user has
explicitly asked. `clawnify init` (type `app`) scaffolds either a
`blank` template (one GET route + a page) or a `crud` template (a
table with list/create/update/delete + a form). Both share the exact
same stack below.
## File layout
```
my-app/
clawnify.json ← Manifest (validated against /schema/v1/clawnify.json)
package.json
tsconfig.json
vite.config.ts ← @vitejs/plugin-react + @tailwindcss/vite; proxies /api → :8787
index.html
.gitignore
.clawnify/ ← Managed by the CLI (gitignored) — never edit
wrangler.toml ← Generated wrangler config
drizzle.config.ts ← Generated drizzle config (from clawnify.json + schema.ts)
drizzle/ ← Generated migration SQL (applied by dev + deploy)
applied-migrations.json ← Local-apply tracker
src/
server/
index.ts ← OpenAPIHono entry — mounts routes, serves /api/openapi.json
routes.ts ← OpenAPIHono + createRoute definitions — the API surface
schema.ts ← Drizzle table definitions — the single source of truth
worker-env.d.ts ← Cloudflare Workers types reference
uploads.ts ← R2 helpers (only when storage is enabled)
client/
main.tsx ← React mount point
app.tsx ← Root component (plain fetch against /api/*)
index.css ← @import "tailwindcss"
```
## Schema — `schema.ts` is the single source of truth
`schema.ts` (Drizzle DSL) defines the tables. Do **not** hand-write a
`schema.sql` in a scaffolded app, and there is no toolchain config to
maintain: the drizzle config is a managed artifact the CLI materializes
into `.clawnify/` (from `clawnify.json` + the schema path), and the DDL
is generated from `schema.ts` into `.clawnify/drizzle/*.sql` — which is
what `clawnify dev` applies locally and the deploy applies to your D1.
The scaffold's starter table (blank template):
```ts
// src/server/schema.ts
import { sqliteTable, text, integer } from "@clawnify/db";
export const items = sqliteTable("items", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
createdAt: text("created_at").notNull().$default(() => new Date().toISOString()),
});
```
Real apps are multi-tenant. Every user-facing table carries an
`org_id`, every query filters by it. JSON columns and indexes use the
Drizzle DSL re-exported from `@clawnify/db`:
```ts
import { sqliteTable, text, integer, index } from "@clawnify/db";
export const notes = sqliteTable(
"notes",
{
id: integer("id").primaryKey({ autoIncrement: true }),
orgId: text("org_id").notNull(), // ALWAYS filter by this in queries
title: text("title").notNull(),
body: text("body").notNull().default(""),
tags: text("tags", { mode: "json" }).$type<string[]>().notNull().default([]),
createdAt: text("created_at").notNull().$default(() => new Date().toISOString()),
},
(t) => ({
byOrg: index("notes_by_org").on(t.orgId, t.createdAt),
}),
);
```
Rules:
- JSON columns: `text(col, { mode: "json" }).$type<T>()` — Drizzle
serializes on write and parses on read.
- Multi-tenancy: every user-facing table has `org_id`. Every query
filters by it. No exceptions.
- Identifiers: snake_case in SQL, camelCase in TS (Drizzle bridges).
## Generating the DDL — automatic, or `clawnify db generate`
`clawnify dev` and `clawnify deploy` (local-directory deploys) generate
the migration SQL themselves — editing `schema.ts` and running dev or
deploy is the whole workflow. To refresh the SQL explicitly without
either, run `npx clawnify@latest db generate` (the scaffold's
`pnpm db:generate` is an alias for it). Always invoke the CLI as
`npx clawnify@latest` — never a global or pinned install. Deploy refuses a database-enabled app with no DDL,
so a broken generate fails loudly, not silently.
One artifact to leave alone: after a successful signed-in deploy the CLI
syncs a read-only `schema.sql` snapshot of the DEPLOYED schema into the
app root. It is generated output for reference/diffing — never edit it,
and never treat it as the source of truth (`schema.ts` is).
Rules:
- Deploys are **additive**: new tables and columns are added
automatically. Destructive changes (dropping or retyping a column) are
not applied automatically — do those deliberately, and never drop a
table with user data unless explicitly asked.
- Never edit the generated `.clawnify/drizzle/*.sql` by hand — change
`schema.ts` and regenerate.
- A hand-written `schema.sql` at the app root is accepted **only** for
apps that don't use Drizzle at all (e.g. some template repos). A
scaffolded app never needs one.
## `src/server/index.ts` — the entry
```ts
import { createApp } from "@clawnify/app";
import api from "./routes";
// db:false — this app reads the database per request via getDB(c.env),
// which needs no init middleware.
const app = createApp({ title: "My app", version: "1.0.0", db: false });
app.route("/", api);
export default app;
```
The entry is thin: `createApp()` then mount your routes. `createApp`
returns an `OpenAPIHono` with **API discovery already mounted** — it
serves both `/api/openapi.json` (generated from your `createRoute`
definitions, with schemas) **and** `/llms.txt` (the same, agent-readable).
Any `/api/*` route written as a plain `app.get` with no `createRoute` is
still listed from the live route table, so the endpoint inventory is
always complete. **Never hand-write a `/openapi.json` or `/llms.txt`
handler** — and don't call `mountDiscovery` yourself; `createApp` did it.
When storage (file uploads) is enabled, the entry also wires the R2
bucket in a middleware before mounting routes:
```ts
import { createApp, type AppEnv } from "@clawnify/app";
import { initUploads } from "./uploads";
import api from "./routes";
type Env = AppEnv & { Bindings: { UPLOADS: R2Bucket } };
const app = createApp<Env>({ title: "My app", version: "1.0.0", db: false });
app.use("*", async (c, next) => {
initUploads(c.env.UPLOADS);
await next();
});
app.route("/", api);
export default app;
```
## `src/server/routes.ts` — routes with `createRoute`
Each endpoint is a `createRoute({ method, path, summary, request, responses })`
definition paired with a handler via `api.openapi(route, handler)`.
Request bodies and params are Zod-validated (`c.req.valid("json")`,
`c.req.valid("param")`); response shapes are declared with
`.openapi("Name")` Zod schemas so they land in the generated spec.
Blank template — one read route:
```ts
import { OpenAPIHono, createRoute, z, PageQuery, pageParams, pagedResponse, type AppEnv } from "@clawnify/app";
import { getDB, desc, like } from "@clawnify/db";
import * as schema from "./schema";
const api = new OpenAPIHono<AppEnv>();
const ItemSchema = z.object({
id: z.number(),
name: z.string(),
createdAt: z.string(),
}).openapi("Item");
api.openapi(
createRoute({
method: "get",
path: "/api/items",
summary: "List items",
// Every list endpoint pages: an agent reads this API through one tool,
// and an unbounded list would land in its context whole.
request: { query: PageQuery },
responses: { 200: pagedResponse("items", ItemSchema) },
}),
async (c) => {
const { page, limit, offset, search } = pageParams(c.req.valid("query"));
const db = getDB(c.env, { schema });
const where = search ? like(schema.items.name, "%" + search + "%") : undefined;
const total = await db.$count(schema.items, where);
const items = await db
.select()
.from(schema.items)
.where(where)
.orderBy(desc(schema.items.createdAt))
.limit(limit)
.offset(offset);
return c.json({ items, total, page, limit }, 200);
},
);
export default api;
```
CRUD template — the full list/create/update/delete surface:
```ts
import { OpenAPIHono, createRoute, z, PageQuery, pageParams, pagedResponse, type AppEnv } from "@clawnify/app";
import { getDB, eq, desc, like } from "@clawnify/db";
import * as schema from "./schema";
const api = new OpenAPIHono<AppEnv>();
const ItemSchema = z.object({
id: z.number(),
title: z.string(),
status: z.string(),
createdAt: z.string(),
}).openapi("Item");
const CreateItemSchema = z.object({ title: z.string().min(1) }).openapi("CreateItem");
const UpdateItemSchema = z.object({
title: z.string().min(1).optional(),
status: z.string().optional(),
}).openapi("UpdateItem");
const IdParamSchema = z.object({ id: z.string() });
const OkSchema = z.object({ ok: z.boolean() }).openapi("Ok");
api.openapi(
createRoute({
method: "get",
path: "/api/items",
summary: "List items",
// Every list endpoint pages: an agent reads this API through one tool,
// and an unbounded list would land in its context whole.
request: { query: PageQuery },
responses: { 200: pagedResponse("items", ItemSchema) },
}),
async (c) => {
const { page, limit, offset, search } = pageParams(c.req.valid("query"));
const db = getDB(c.env, { schema });
const where = search ? like(schema.items.title, "%" + search + "%") : undefined;
const total = await db.$count(schema.items, where);
const items = await db
.select()
.from(schema.items)
.where(where)
.orderBy(desc(schema.items.createdAt))
.limit(limit)
.offset(offset);
return c.json({ items, total, page, limit }, 200);
},
);
api.openapi(
createRoute({
method: "post",
path: "/api/items",
summary: "Create item",
request: { body: { content: { "application/json": { schema: CreateItemSchema } } } },
responses: {
201: {
description: "Created item",
content: { "application/json": { schema: ItemSchema } },
},
},
}),
async (c) => {
const { title } = c.req.valid("json");
const db = getDB(c.env, { schema });
const [row] = await db
.insert(schema.items)
.values({ title: title.trim() })
.returning();
return c.json(row, 201);
},
);
api.openapi(
createRoute({
method: "patch",
path: "/api/items/:id",
summary: "Update item",
request: {
params: IdParamSchema,
body: { content: { "application/json": { schema: UpdateItemSchema } } },
},
responses: {
200: { description: "Updated", content: { "application/json": { schema: OkSchema } } },
400: { description: "Nothing to update", content: { "application/json": { schema: OkSchema } } },
},
}),
async (c) => {
const { id } = c.req.valid("param");
const body = c.req.valid("json");
const patch: Partial<typeof schema.items.$inferInsert> = {};
if (body.title !== undefined) patch.title = body.title;
if (body.status !== undefined) patch.status = body.status;
if (Object.keys(patch).length === 0) return c.json({ ok: false }, 400);
const db = getDB(c.env, { schema });
await db.update(schema.items).set(patch).where(eq(schema.items.id, Number(id)));
return c.json({ ok: true });
},
);
api.openapi(
createRoute({
method: "delete",
path: "/api/items/:id",
summary: "Delete item",
request: { params: IdParamSchema },
responses: {
200: { description: "Deleted", content: { "application/json": { schema: OkSchema } } },
},
}),
async (c) => {
const { id } = c.req.valid("param");
const db = getDB(c.env, { schema });
await db.delete(schema.items).where(eq(schema.items.id, Number(id)));
return c.json({ ok: true });
},
);
export default api;
```
Patterns to follow:
- **Give every route a clear `summary`/`description`.** The generated
OpenAPI spec is what the agent reads to decide which endpoint to
call — a vague summary makes your endpoint invisible in practice.
- **Validate input with Zod** via `request.body` / `request.params`
and read it with `c.req.valid("json")` / `c.req.valid("param")`.
Never parse `c.req.raw` by hand.
- **Declare response shapes** with `.openapi("Name")` schemas — they
become the documented return type in the spec.
- **Filter by the org** on every user-facing query (see "who is
calling" below). No exceptions.
## `src/server` — who is calling
The platform authenticates every caller at its perimeter and injects a
verified identity onto the request (it strips any client-supplied
version first, so it can't be spoofed). Your app never authenticates
anyone: no `Authorization`, no cookies, no JWT, no login screen.
Read it with the helpers from `@clawnify/app` — don't parse headers
yourself:
```ts
import { user, orgId, caller } from "@clawnify/app";
app.get("/api/notes", async (c) => {
const org = orgId(c);
if (!org) return c.json({ error: "unauthorized" }, 401);
const db = getDB(c.env, { schema });
return c.json(
await db.select().from(schema.notes).where(eq(schema.notes.orgId, org)),
);
});
```
| Helper | Returns |
|--------|---------|
| `orgId(c)` | Organization UUID — the tenant key. Filter every user-facing query by it. |
| `user(c)` | `{ id, email, name, avatarUrl, firstName, lastName }` — or **`null`** when the caller isn't a person. |
| `caller(c)` | `user`, `api`, `agent`, `agent-browser`, `public`, `bypass`, `system`, or `app`. |
Two rules that catch people out:
- **`user(c)` is null for five of the eight caller kinds**
(`agent-browser`, `public`, `bypass`, `system`, `app`). Always handle
the null case — don't assume a person is present just because a
request arrived. For per-user rows, store `user(c).id` in a `user_id`
column and filter on it, exactly as you filter on `org_id`.
- **`firstName` / `lastName` are a heuristic** split of a single stored
full name (first word / the rest). Fine for a greeting; never persist
them or treat them as someone's legal name — store `name`.
Don't build app-level users, roles, or permissions. Authorization
already happened at the perimeter.
## `src/client` — React + plain fetch
The client mounts React and talks to the API with plain `fetch`
against the `/api/*` routes. No data-layer library is scaffolded.
```tsx
// src/client/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import "./index.css";
createRoot(document.getElementById("app")!).render(
<StrictMode>
<App />
</StrictMode>
);
```
```tsx
// src/client/app.tsx (CRUD template shape)
import { useState, useEffect } from "react";
interface Item {
id: number;
title: string;
status: string;
createdAt: string;
}
export function App() {
const [items, setItems] = useState<Item[]>([]);
const [title, setTitle] = useState("");
async function load() {
const res = await fetch("/api/items");
setItems(((await res.json()) as { items: Item[] }).items);
}
useEffect(() => { load(); }, []);
async function addItem(e: React.FormEvent) {
e.preventDefault();
if (!title.trim()) return;
await fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
setTitle("");
load();
}
// ...toggleStatus / deleteItem call PATCH/DELETE the same way.
return (
<div className="max-w-xl mx-auto mt-10 px-4">
{/* form + list */}
</div>
);
}
```
In local dev, Vite proxies `/api` to the Worker on `:8787` (see
`vite.config.ts`), so the same relative fetch paths work in dev and
in production. The API is the one contract: the UI calls it over
`fetch`, and the agent calls it over the OpenAPI spec.
## Design
If the project contains a `DESIGN.md`, it is the single source of
truth for visuals — follow its tokens, don't restyle around it. The
scaffold ships Tailwind v4 (via `@tailwindcss/vite`, imported with
`@import "tailwindcss";` in `index.css`) and nothing else — no
component library. The rules every Clawnify app follows:
- **Neutral chrome.** White canvas and surfaces, one text ramp
(foreground → muted → faint), hairline borders. Color is for
meaning, not decoration.
- **One primary CTA per screen.** Blue is reserved for links and
focus rings; never a second accent.
- **Status colors are muted pairs** — never pure-saturated fills.
- **Style with Tailwind utilities.** Don't install another component
library or write a bespoke CSS system unless the user asks.
- **Sizes in rem**, so browser zoom and font settings are respected.
## `clawnify.json` — the manifest
```json
{
"$schema": "https://app.clawnify.com/schema/v1/clawnify.json",
"version": 1,
"name": "Notes",
"description": "Take and organize notes. Searchable by tag.",
"app": {
"framework": "react+hono",
"database": true,
"storage": false
},
"env": {
"optional": ["OPENROUTER_API_KEY"]
},
"api": {
"public_routes": [
{ "path": "/api/feed", "methods": ["GET"] }
]
}
}
```
`clawnify init` writes the minimal form:
`{ app: { framework: "react+hono", database: true, storage } }`. Add
the rest as needed.
If the app talks to third-party services the user has connected
(ads platforms, CRMs, messaging), declare them:
```json
{ "app": { "credentials": ["metaads", "googleads"] } }
```
Get the ids and names from `clawnify connections list` — see the
naming rule below.
`env` supports `required` (deploy warns if missing), `optional`
(injected when available), and `oneOf` (at least one per group).
Rules:
- **There is no `api.tools[]`.** The manifest's `api` object only
accepts `public_routes` and `automation_bypass`. Your API surface
is discovered from the generated OpenAPI spec, not a hand-authored
tools list (see the next section).
- **Public routes are the exception**, not the default. Every route
is behind the Clawnify perimeter (auth-required) unless you
explicitly add a matching path to `api.public_routes`. Use only for
webhooks, RSS, embedded feeds.
- **`framework: "react+hono"`** for new apps. Never `preact+hono` or
`vite-preact` — those are legacy.
- **`database: true`** if the app uses a DB. `schema.ts` defines the
tables; run `pnpm db:generate` after changing it.
## How the agent reaches your API (no `api.tools[]`)
The old tRPC stack emitted an `api.tools[]` array from procedure meta.
That mechanism is gone. Today the flow is:
1. Your `createRoute` definitions auto-generate a live OpenAPI 3.0 spec
plus a lean `/llms.txt` index, both served for you — never hand-write
or route them yourself. **These are the agent's discovery channel:**
it reads `GET /llms.txt` (index of every endpoint) or
`GET /api/openapi.json` (full request/response schemas) on the live
app. Because both are generated from the live route table, they
cannot drift from the code.
2. The agent then drives your endpoints on the live app. A Clawnify
agent uses `clawnify_call_app_api`; Claude, ChatGPT and other MCP
clients use `clawnify_app_api_read` (GET), `clawnify_app_api_write`
(POST, PUT, PATCH) and `clawnify_app_api_delete`. No per-app MCP
registration and no manifest tools array. Use source reading to
*modify* code, not to discover endpoints.
So you make an endpoint "agent-callable" by defining it with
`createRoute()` + `app.openapi()` and giving it a clear
`summary`/`description` with Zod-typed request/response. The clearer
those are, the more reliably the agent uses it.
### `agent.md` — lead with the procedure, not the endpoint list
`agent.md` is extracted into `agent_instructions` on every build. Since
discovery is already covered by the generated surfaces above, **`agent.md`
is not an API reference** — re-listing every payload there just creates a
second source of truth that goes stale the first time a route changes.
It is the one place for what generation can't produce: **judgment**.
Write it in this order:
1. **Division of labour, first and short.** What the agent should do
itself vs. what it must delegate to the app — and say the wrong move
out loud (*"never look up contact details yourself; post the records
and let the app resolve them"*). This one block prevents more misuse
than the rest of the file combined.
2. **The procedure** — numbered steps for the app's main job, written so
the agent can follow them on a request it has never seen.
3. **Pages**, flagging the screenshot-friendly ones.
4. **API anchors, not API docs** — method, path, and *when to reach for
it*, plus the one or two request shapes the agent writes most often.
For anything else, let it read `/llms.txt`.
5. **How to read failures**, and any **cost discipline** — which calls
spend the user's money and which are free.
**Don't ship a per-app skill.** A skill's frontmatter stays resident in
agent context, so one skill per app grows the agent's context floor as
the app count grows; `agent_instructions` and `/llms.txt` are pulled on
demand instead. Skills are for **cross-app** procedures — if one earns
reuse across more than one app, promote that single procedure
(parameterized) to the shared skills library rather than copying it into
every app.
## Connections & secrets — `@clawnify/connections`
**The naming rule — never invent an identifier.** Before declaring
`credentials` or env names, run `clawnify connections list`: it shows
the org's canonical integration ids (`googleads`, never `google_ads`),
the fixed provider-key names (`OPENROUTER_API_KEY`, never
`OPENROUTER_KEY`), and custom env vars already defined — each with
connected/set state. Reuse those exact identifiers; a misspelled one
doesn't fail the build, it silently resolves to nothing at runtime. If
you need a custom secret that isn't listed, pick a clear name and tell
the user to set it in the dashboard.
If the app needs a third-party service or a provider API key, use
the `@clawnify/connections` SDK. Never read credential bindings
directly, never paste API keys, never hardcode a token or endpoint.
```ts
import { connect, secret, describe, searchActions } from "@clawnify/connections";
// A connected integration:
const meta = connect("metaads", env);
const accounts = await meta.adAccounts("name,currency");
// A provider key the user supplied (null if absent):
const key = secret("OPENROUTER_API_KEY", env);
// What's wired vs missing — drive your setup/empty state from this:
const status = await describe(env, undefined, REQUIRES);
```
How to call a connected service — the mistake that silently breaks
integration apps, so get it right:
- **Prefer the typed client's semantic methods** where they exist
(`meta.adAccounts(...)`, `googleads.query(gaql)`).
- **For anything else, discover and run an action:**
```ts
const [best] = await searchActions("list google ads campaigns", env);
const data = await connect(best.service, env).run(best.slug, body);
```
- Some integrations also expose a raw REST client
(`connect("bird", env).get("/path")`) — but if `.get()`/`.post()`
throws for a service, that service only supports semantic methods
and `run()`. When unsure, use `run()`.
- **Need exact paths/arguments for a raw-REST provider?** Some carry
a `spec_url` (the provider's published OpenAPI spec) —
`clawnify connections list --json` shows it. Download and query it —
`curl -s <spec_url> -o spec.json && jq '.paths | keys' spec.json` —
never read a whole spec into context (they run to 10+ MB).
- **Never fall back to raw `fetch`** against a provider API, and
never invent endpoints or guess tokens.
**The one exception — a service we don't support at all.** If a
provider isn't in the connections registry (nothing to `connect()`
to, and `clawnify connections list` doesn't show it), the sanctioned
path *is* a raw call with a user-supplied key: declare the key(s) it
needs in `clawnify.json` `env` (`required`/`optional`/`oneOf`), tell
the user to set them in the dashboard, read them with
`secret("THAT_KEY", env)`, and `fetch` the provider's documented
endpoints directly. This is the escape hatch for the long tail of
integrations we don't model yet — it does **not** apply to a service
we *do* support (use `connect()` / `run()` for those, always). Keep
the endpoint and token server-side, never in the client bundle.
Build a `describe()`-driven setup state instead of assuming
connections exist — show the user what's missing and where to
connect it.
## Scheduling & deferred work — the managed queue
A deployed app **cannot bind Cloudflare Queues, Durable Objects, or cron
triggers** (the platform builds every app with a fixed binding set: `DB` +
`ASSETS` + `UPLOADS`). To run work later — a scheduled publish, a reminder, a
retry, a "do this in 10 minutes" — use the **Clawnify managed queue service**
over HTTP. It owns the clock, retries, and at-least-once delivery, then calls an
endpoint on *your own app* back at the scheduled time.
Use the published `@clawnify/queue` package — `enqueueJob`, `cancelJob`,
`verifyDelivery` — same shape as `@clawnify/db` / `@clawnify/connections`.
Auth is the build-injected `env.CLAWNIFY_TOKEN`; with no token (local dev)
it degrades to a no-op.
**Enqueue** a deferred call to your own endpoint:
```ts
// POST https://services.clawnify.com/queue/enqueue (Bearer CLAWNIFY_TOKEN)
const res = await fetch("https://services.clawnify.com/queue/enqueue", {
method: "POST",
headers: { Authorization: `Bearer ${env.CLAWNIFY_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
target_url: `${origin}/api/internal/publish`, // an endpoint on THIS app
payload: { post_id: id }, // echoed back to you on fire
run_at: new Date(whenMs).toISOString(),
idempotency_key: `post:${id}:${whenMs}`, // same key dedupes; new time = new job
}),
});
const { job_id } = await res.json(); // store it to cancel/reschedule
```
Persist `job_id` on the row (e.g. `posts.queue_job_id`). **Cancel / reschedule**:
`DELETE https://services.clawnify.com/queue/jobs/{job_id}` (Bearer token);
reschedule = cancel + a fresh enqueue.
**The callback endpoint is a public, signature-verified route.** The queue calls
`target_url` from outside the perimeter, so the endpoint must be declared in
`clawnify.json` `public_routes` **and** verify the delivery signature itself —
the signature *is* its auth (never trust an unsigned call):
```jsonc
// clawnify.json
"api": { "public_routes": [ { "path": "/api/internal/publish", "methods": ["POST"] } ] }
```
Deliveries are signed with the platform's **ECDSA P-256 (ES256)** key,
Stripe-style over `${timestamp}.${rawBody}`, verified against
`https://services.clawnify.com/.well-known/jwks.json` (public key — nothing
breaks on token rotation). Read the raw body, check the signature + a timestamp
tolerance (~5 min) before doing anything:
```ts
app.post("/api/internal/publish", async (c) => {
const raw = await c.req.text();
const ok = await verifyDelivery(raw, {
signature: c.req.header("x-queue-signature"),
timestamp: c.req.header("x-queue-timestamp"),
keyId: c.req.header("x-queue-key-id"),
});
if (!ok) return c.json({ error: "bad signature" }, 401);
const { post_id } = JSON.parse(raw);
// …do the deferred work (short: it's one normal Worker invocation)…
return c.json({ ok: true });
});
```
Each fire is a normal Worker request — size the work to one invocation (publish a
post, send a digest). For long compute *now* (not deferred), hold the request
open and return when done; don't fire-and-forget past the response.
**When the queue says no.** A refused enqueue answers `{ "error", "detail" }`
(`enqueueJob` throws `ClawnifyQueueError` with `code`, `status` and `message`).
Past the plan's monthly included jobs each job costs credits, so only an empty
balance (`402 insufficient_credits`) or no active plan (`403 quota_exceeded`)
refuses one; show its `detail` to the owner as it is, since it says where to fix
it. A job that books the next one (a daily sync) must never swallow that
refusal: keep it, show it as a banner on the app's main page, and return it from
the app's status endpoint. Every service error code:
https://docs.clawnify.com/concepts/service-errors
## Migrations
The workflow is:
1. Edit `src/server/schema.ts` for the new shape (types + queries).
2. Run `pnpm db:generate` — emits the migration SQL under
`.clawnify/drizzle/`.
3. Run `npx clawnify@latest dev` locally, then `npx clawnify@latest deploy`.
On deploy your schema changes are applied to the app's database — new
tables and columns are added automatically.
Destructive changes (drop column, rename) are not applied automatically;
do them deliberately, and only when the user explicitly asks. Renames
are risky on production data — prefer "add new column, backfill, switch
queries, drop old column" over a single-step rename.
## What the agent must NOT do
- Author a `/openapi.json` or `/llms.txt` handler by hand. `createApp()`
mounts both, generated from your `createRoute` definitions.
- Hand-write a `schema.sql`, edit anything under `.clawnify/`, or add a
root `drizzle.config.ts`. Schema changes happen in `schema.ts`; the
CLI generates and applies the SQL.
- Invent an `api.tools[]` array in the manifest. It isn't a valid
field — the OpenAPI spec is the tool surface.
- Use `JSON.stringify` on query params. JSON columns auto-serialize
via Drizzle. The raw SQL API has `json()` as a helper.
- Skip the `org_id` filter on a user-facing query. Multi-tenant
leak is the worst-class bug we can ship.
- Return a whole table from a list endpoint, or a bare array. Every list
pages: `PageQuery`, `pageParams` and `pagedResponse` from `@clawnify/app`.
- Read auth from `Authorization` / cookies / `Bearer` tokens, or
hand-parse `X-Clawnify-*` headers. Use `user(c)` / `orgId(c)` /
`caller(c)` from `@clawnify/app`.
- Call third-party APIs with raw `fetch` or hardcoded tokens. Go
through `@clawnify/connections` (`connect` / `secret` / `run`).
- Install `drizzle-orm` as a separate top-level dependency.
`@clawnify/db@^0.4.1` pulls it in transitively; only add it if you
need to pin a specific version (rare).
## What the agent SHOULD do
- Import everything from `@clawnify/db` where possible
(`getDB`, `eq`, `and`, `desc`, `sql`, `sqliteTable`, `text`,
`integer`, `index`, etc. are all re-exported there). Drop to
`drizzle-orm` directly only for things not re-exported.
- Give every route a clear `summary`/`description` and Zod-typed
request/response so the agent can discover and call it via the
OpenAPI spec.
- Page every list endpoint with the helpers `@clawnify/app` exports:
`request: { query: PageQuery.extend({ /* filters */ }) }`,
`const { page, limit, offset, search } = pageParams(c.req.valid("query"))`
(25 per page by default, capped at 100, bad input falls back), and
`responses: { 200: pagedResponse("items", ItemSchema) }` for
`{ items, total, page, limit }`. Count with `db.$count(table, where)`.
- Keep a long text field an agent edits again and again (HTML, a
document body, a template) as one top-level string of the record,
with GET and a *partial* update (`PATCH`, or a PUT that keeps the
fields it is not sent) on the same path, returning the updated
record. Agents then read it in parts and change it with literal
`old_text` → `new_text` edits instead of sending all of it back; the
platform provides that, so don't build a find/replace endpoint.
- Type a helper that takes the database client as `DB<typeof schema>`
(from `@clawnify/db`), and a separate routes file's env as `AppEnv`
(from `@clawnify/app`). Never name the backend (`D1Database`,
`DrizzleD1Database`) or cast `getDB`'s result: the same app code is
served by more than one database backend.
- Test locally with `npx clawnify@latest dev` before deploying; deploys go live at
`https://<slug>.apps.clawnify.com` (the dashboard's preview pane shows
the draft tier).
- When unsure about a Drizzle pattern, check the `@clawnify/db`
README for the current API + version notes.
## Schema-as-data exception
Apps that let END users define data shapes at runtime (CMS, no-code
table editor) can't model their schema in `schema.ts` — the table
definitions live in rows. Those apps fall back to the raw SQL API
from `@clawnify/db` (`query`, `get`, `run`, `json`). Open-cms is
the canonical example.
If you're building this kind of app, you still use Drizzle's
`schema.ts` for the **registry** tables (content types, field
definitions). Only the dynamically-spawned per-collection tables
need raw SQL. See open-cms's `src/server/schema-sync.ts` for the
runtime DDL pattern.
## When you're stuck: compare with a working app
Every `Open*` repo under [github.com/clawnify](https://github.com/clawnify)
is a complete, open-source app running on this platform. When your
app hits an error you can't resolve from this document, find one that
does the same thing and compare how it does it.
- **Pick the repo by what it uses.** Each repo's `clawnify.json`
declares it: `app.credentials` lists its integrations (`metaads`,
`googleads`, ...), `app.database` and `app.storage` say whether it
uses D1 and R2. The README says what the app does.
- **Read it without signing in.** List the repos at
`https://api.github.com/orgs/clawnify/repos?per_page=100`, and fetch
any file at `https://raw.githubusercontent.com/clawnify/<repo>/HEAD/<path>`
(`HEAD` resolves the default branch, which is not always `main`).
- **Copy the solution, not the layout.** This document wins on
conventions. Many templates predate parts of it: most hand-write
`src/server/schema.sql` instead of `schema.ts`, some use Preact, and
older ones don't page their lists with the `@clawnify/app` helpers.
- **If the error comes from a `@clawnify/*` package**, read what you
installed: `node_modules/@clawnify/<pkg>/README.md` and
`dist/index.d.ts`, which match your version exactly.
build-a-clawnify-website7.79 KB
---
name: build-a-clawnify-website
description: Build and ship a Clawnify website — Astro on Cloudflare Workers, the built-in form system (never hand-roll a form), images and assets, and the draft-then-publish deploy flow. Use when creating or editing a Clawnify website, or when a site needs a contact/waitlist/signup form.
---
# Build a Clawnify website
A Clawnify website is an **Astro** project deployed to Cloudflare Workers. One
site per organization. You write pages; the platform owns hosting, forms and
the publish flow.
```bash
npx clawnify@latest init --type website --name "My Site" website
cd website && pnpm install && pnpm dev # http://localhost:4321
```
Always invoke the CLI as `npx clawnify@latest <command>` — never a
global or pinned install; stale CLIs miss deploy guards and fixes.
```
website/
src/pages/ # one .astro file per route (index.astro → /)
src/layouts/ # shared shell: nav, footer, <head>
src/blocks/ # platform blocks you've adopted (see Forms below)
public/ # images, favicons — served as-is from /
```
## Forms: use the built-in system. Do not build your own.
**This is the single most common mistake.** A site needs a contact or waitlist
form, and the obvious move is to write a `<form>` and POST it somewhere — your
own endpoint, a third-party form service, or the platform's submit URL with the
website id pasted in. All three are wrong, and the last one looks right enough
to survive review.
Clawnify has a form system. It captures submissions, stores them, can email you,
and can hand each one to an agent or a flow. You get all of it by using the
**Forms block** rather than writing a form yourself.
### How it works
- The submit target is **computed at render time** from `WEBSITE_ID` — a
variable the platform bakes into your site's Worker — plus the form's id:
```astro
const env = (Astro.locals as { runtime?: { env?: Record<string, string> } })?.runtime?.env;
const websiteId = env?.WEBSITE_ID ?? "";
const action = websiteId ? `https://provision.clawnify.com/forms/submit/${websiteId}/${formId}` : "";
```
**Never hardcode the website id.** It is baked per Worker so a site can only
ever submit to itself — hardcoding throws that away, and the literal goes
stale the moment the site is recreated. An empty `action` in local `astro dev`
is expected: there are no Worker vars locally, so the form simply no-ops.
- The submit script POSTs JSON with `Content-Type: text/plain;charset=UTF-8`.
That is not a typo — `text/plain` is CORS-safelisted, so the request needs no
preflight, which matters because the platform can't know your site's origin in
advance. The route parses the text body as JSON.
- **Keep the Turnstile widget.** The ingest rejects a submission with no token,
so a form without the widget fails every time with "Verification required".
The sitekey shipped in the block is public and pairs with the platform's
secret; the widget will not validate on `localhost`, only on a real hostname.
### Getting the block
Sites are seeded with the platform's block library at creation. If yours has
`src/blocks/Forms/Contact.astro`, import it and go. If it doesn't — an older
site, or one scaffolded bare by the CLI — copy it in from the website template
and adapt it.
**The design is yours; the plumbing is not.** Restyle the markup, labels and
classes freely. Preserve, exactly:
- the computed `action` (never a prop, never a literal),
- the Turnstile widget,
- the `data-clawnify-managed-form` attribute,
- `export const schema`,
- destructuring only known props — never splat leftover props onto `<form>`,
or an injected `action` becomes a way to point your form at someone else.
There is no submit script to copy. The runtime lives in the framework catch-all
(`src/pages/[...path].astro`) and binds every `data-clawnify-managed-form` on
the page. If the block you inherited still has its own inline `<script>` and a
`data-clawnify-form` attribute, it is the older shape: it still works, and you
can migrate it by deleting the script and renaming the attribute. Do not carry
both attributes on one form, and do not add `data-clawnify-form` to a managed
form: older blocks bind that selector unconditionally, so the form would submit
twice.
### After a successful submit
Two options, and the default is unchanged.
- **Inline message** (default). The `successText` prop, rendered into the
status line.
- **Redirect.** Set the `successUrl` prop, or `data-success-url` on any form
you hand-built. A site-relative path (`/thanks`) or an absolute `https://`
URL (a booking page is fine). Anything else is ignored and the message is
shown instead, which is what keeps `javascript:` out of the navigation.
A redirect to a thank-you page is also the only way to count a form conversion
in the analytics tool, which has no custom events: the dashboard Submissions
panel counts submissions, but a pageview is what gives you a rate.
Need a side effect on success, like setting a cookie? Listen for the
`clawnify:form-success` event on the form. It is cancelable, so
`preventDefault()` suppresses the redirect if you would rather navigate
yourself.
### Fields
The stock block collects **name, email and message**. Two ways to go beyond
that, and the right one depends on whether your form is registered:
- **Registered forms** (a tree node named `Forms/Contact`) are schema-locked:
the ingest 422s any field it doesn't expect. Give extra inputs **no `name`
attribute** and have the submit script fold them into `message`.
- **Unregistered forms** — hand-built pages like this one — are not locked, so
extra fields post as themselves and are stored verbatim in the payload. Better
data, but if the site later moves onto the tree renderer, registration will
start rejecting them. Leave a comment saying so.
### PDF attachments
The Contact block can collect one PDF per submission (job applications, RFQs).
Off by default — enable it with the block's `attachment` prop (and optionally
`attachmentLabel`), then **publish**: the file field is registered at publish
time, and the ingest rejects files on forms that haven't registered one. Rules
the platform enforces server-side (don't fight them client-side): PDF only,
verified by content not by extension, max 10 MB. The submission's payload
carries the filename and a download link that only the site's own organization
can open — the file is never on a public URL. When a file is attached the
submit script posts `multipart/form-data` instead of JSON; both are
preflight-free, and the block already handles the switch.
### Where submissions go
Every submission is stored and visible in the dashboard. From there it can
notify an email address, or fan out to an agent or a flow — which is how a
signup becomes a CRM record or a WhatsApp message without you writing a backend.
Set the notification address in the dashboard, not in the site.
## Images and assets
Put them in `public/` and reference them with an absolute path (`/img/hero.jpg`).
They're served as static files. Pull in real assets at real resolution rather
than linking someone else's CDN from your markup.
## Deploying: draft first, then publish
```bash
npx clawnify@latest deploy # → the DRAFT site, visible only to your org
```
Deploying **never** changes the live site. Going live is a separate, explicit
step: review the draft, then **Publish to live** in the dashboard. Tell the user
this — a deploy that "worked" but didn't appear on the live site is almost
always someone expecting deploy to publish.
## Pinning the org
Every command resolves which organization it targets, folder-first. In a linked
folder you get it for free; otherwise pass `--org`. If you work across several
organizations, pass `--org` explicitly on anything that writes — the global
active org is shared machine-wide and another session can change it under you.
clawnify-knowledge2.54 KB
--- name: clawnify-knowledge description: Route a question to the right Clawnify knowledge source before answering or building. Covers how-to-use-Clawnify product docs, your org's company knowledge (brand, pricing, policy, SOPs), a specific client or project's reference, and durable memory. Use whenever a task needs a company fact, a platform how-to, or client-specific context from a connected Clawnify organization. --- # Clawnify knowledge router You are connected to a Clawnify organization through the Clawnify connector. Several knowledge sources sit behind it, and each answers a different kind of question. Pick the right one and retrieve from it: these sources are current and scoped to this organization, and your training data is neither. ## Route by question type | The question is about | Use | Notes | |---|---|---| | How to use Clawnify itself: deploy an app, set up an agent, connections, platform features | docs.clawnify.com | Product docs; `https://docs.clawnify.com/llms.txt` indexes them. Check them before building against the platform. | | Which of the org's apps handles something | `clawnify_docs_search` | Searches the org's apps by name and description. | | The company's own facts: brand, pricing, policy, SOP, legal, FAQ | `company_knowledge_search`, then `company_knowledge_get` | The org's source of truth, a versioned wiki linked with `[[wikilinks]]`. | | One specific client, engagement or project | `projects_list`, then `project_search` | Working reference for that project, not org-wide authority. | | Something decided or learned before | `memory_recall` | Save durable facts back with `memory_write`, and `memory_pin` the ones that matter most. | | A third-party library, framework or API | That vendor's own docs | Not in the Clawnify corpus. | | Code in the repository you are working in | Read the code | The code beats any summary of it. | ## Rules - **Retrieve, don't recall.** For a company, product or client fact, query the source. Never answer a Clawnify fact from memory of training data. - **Cite company knowledge.** An answer grounded in a `company_knowledge` result cites it as `Per <Title> v<N>`. - **A proposal is not published.** `company_knowledge_propose` creates a draft that a person reviews. Never present a proposal as a live fact. - **No secrets or per-customer facts in company knowledge.** Those belong in a project, or nowhere. - **Look before you build.** Before creating an app or an agent, check docs.clawnify.com for the current contract and `clawnify_list_apps` or `clawnify_list_agents` for what already exists.
organize-company-knowledge10.5 KB
---
name: organize-company-knowledge
description: >-
Turn a dump of raw company files into a clean, foldered, citable Company
Knowledge corpus — the company's brain. Use when the user has dropped many
files into the Company Knowledge section (or connected a Drive) and wants the
agent to distill and organize them: cluster by topic, distill each into a
canonical markdown doc (merging across sources into reusable playbooks), file
it into a folder, tag and link it, and propose it for review.
metadata:
clawnify:
displayName: Organize my knowledge
trigger: manual
inputs: []
---
# Organize Company Knowledge
The user dumped a pile of files and wants them turned into the company's
**brain** — organized, official Company Knowledge that every agent can draw on
and cite. Your job is to **curate, not mirror**: distill the material that is
*answer authority or reusable know-how* (policies, pricing, brand, SOPs, product
facts, FAQs, legal — **and the playbooks hidden inside the company's own
deliverables**) into a small set of clean markdown docs, each filed into the
right folder and linked to its neighbours — then hand the result to a human to
publish.
Nothing you create goes live automatically. You **propose**; a person reviews
and publishes from the dashboard.
## The mental model (read this first)
- **This builds the company brain, not a client filing cabinet.** Company
Knowledge is the org-wide corpus every agent retrieves from and cites (`Per
<Title> v<N>`). The goal of organizing a dump is to grow that shared brain.
Almost everything answer-authority or reusable belongs here.
- **Deliverables are the richest brain material — distill the PATTERN, not the
client.** An agency's dump is mostly *deliverables*: proposals, strategies,
case studies, decks. A single raw proposal is not company fact — but the
**pattern across all of them** ("how we structure a winning proposal", "how we
run a festival digital strategy") is exactly the company's intelligence, and it
IS citable. So **cluster deliverables across clients into canonical playbooks**
— ONE "Proposal Playbook" distilled from seventeen proposals, not seventeen
client-named docs. Strip the client-specific numbers; keep the reusable method.
- **Client-scoped facts are the rare exception, and you default away from them.**
Only genuinely client-bound working facts (a specific client's budget, contacts,
live campaign state) are *not* brain material. Those belong in that client's
**Project** — but only when the org actually runs a per-client agent that needs
them. Do **not** route deliverables there to "keep them separate": that buries
the brain in silos. When unsure, it goes in the **brain** (the corpus).
- **Three axes inside Company Knowledge, kept separate.** A doc has:
- a single **`folder`** — its ONE home, a slash-nested path like
`playbooks/strategy`, shown as the browse tree (a doc appears in exactly one
place). Omit for a top-level (Unfiled) doc.
- many **`tags`** — cross-cutting labels for search/filter (e.g. `festival`,
`enterprise`). NOT folders. A doc lives in one folder but can carry several
tags.
- optional **`meta`** — structured properties distilled from the material
(e.g. `effective_date`, `owner`). Preserved for the human reviewer.
- **The dump's folder structure is a HINT, not a spec.** Whatever folders the
client shipped (`Training Docs/`, `Strategies/`, `Proposals/`, `Brand/`) are
*signal* about topic — read them, get inspired, reuse the parts already
coherent. But you are curating, not copying: if their structure is messy,
redundant, or one-doc-per-client, **re-derive a cleaner taxonomy** (e.g. collapse
a `Proposals/` folder of per-client files into one `playbooks/` doc). Do not
preserve a structure just because it was theirs.
- **Structure is also links.** Use `[[Doc Title]]` wikilinks between related docs.
These render as the knowledge graph next to the folder tree. Forward references
to docs you haven't written yet are fine — they mark what's worth writing next.
- **Binary assets are not knowledge.** Fonts, logo files, raw images, signed
contract scans — these carry no distillable *answer authority*. Don't force
them into the corpus. Reference them from the relevant doc (e.g. the brand doc
lists the brand fonts and logo files by name); leave the blobs in the file
library.
## Procedure
### 1. Survey the dump and read its structure as signal
- Use `company_knowledge_search` with broad, business-topic queries ("pricing and
discounts", "brand voice and tone", "onboarding SOP", "digital strategy
approach", "proposal structure", "case study results") to pull the salient
content the user dropped in. Files uploaded to the Company Knowledge section are
auto-indexed and searchable to you.
- Note the **folders the dump arrived in** — they hint at topic (a `Proposals/` or
`Strategies/` folder tells you there's a playbook to distill out of many
deliverables). Use them as inspiration, not as the final taxonomy.
> Note: today you read the dumped material through `company_knowledge_search`
> (semantic retrieval). If a `company_files_list` / `company_file_read` tool is
> available to you, enumerate and read each dumped file directly for a more
> complete pass — otherwise work topic-by-topic from search.
### 2. Cluster into canonical brain docs (merge across sources)
Group the material into a SMALL set of **topics**, each becoming one canonical
doc. The key move for a deliverable-heavy dump:
- **Many deliverables of one kind → one playbook.** Seventeen proposals → a
"Proposal Playbook". Nine strategies → a "Digital Strategy Playbook" (split by
segment only if the method genuinely differs, e.g. festivals vs. artists).
- **Training / SOP files → the how-we-work docs** (`doc_type: sop`).
- **Brand deck / overview → brand + positioning docs** (`doc_type: brand`).
- **Prefer a few dense, canonical docs over many thin ones.** If you're about to
write a doc whose title is a client's name, stop — that's a signal you're
mirroring a deliverable instead of distilling the pattern.
### 3. Learn the existing taxonomy BEFORE inventing folders
The single most important step — this is what keeps folders coherent instead of
sprouting `pricing`, `Pricing`, and `price-list` as three bogus folders.
- Call `company_knowledge_list` and read the `folder` values already in use across
the corpus. That set **is** the current folder tree. Note the `tags` too (the
label vocabulary), so you reuse those instead of coining synonyms.
- Reuse existing folders wherever the material fits. Only create a new folder when
nothing existing fits, and prefer a shallow, obvious name (`playbooks`, `sales`,
`brand`, `people`, `legal`, `pricing`, `product`). If the org has no docs yet,
propose a small, conventional top level matching the doc types.
### 4. Distill each cluster → propose it
Call `company_knowledge_propose` once per topic with:
- **`title`** — the canonical name ("Proposal Playbook", "Enterprise Pricing").
A pattern/method name, not a client name.
- **`description`** — one load-bearing line; it drives the knowledge index and
ranking. Always write one: if omitted, the server falls back to the doc's
opening line, which is rarely as good.
- **`content`** — the distilled markdown. A clean, self-contained statement that
**merges the sources and resolves overlaps** — the authoritative method the org
would publish, not a summary of each file. Use `[[Other Doc Title]]` to link
related docs.
- **`doc_type`** — one of policy / sop / pricing / product / brand / faq / legal /
reference.
- **`folder`** — the single home path, e.g. `playbooks/strategy`. Reuse taxonomy
from step 3. Omit for a top-level doc.
- **`tags`** — cross-cutting labels (e.g. `["festival","q3"]`), NOT the folder.
Optional.
- **`meta`** — optional structured properties, e.g.
`{ "owner": "Strategy", "reviewed": "2026-01-01" }`.
- **`source_ref`** — provenance: the source file id(s)/name(s) you distilled from.
**Always set this** so a re-run recognises what's already organized instead of
duplicating, and the reviewer can trace a doc back to its source.
You can also put the metadata in the markdown itself as YAML frontmatter — it is
parsed on save and wins over the fields, so this is equivalent and round-trips
with export (`folder`/`tags` are separate keys; any other key is preserved as a
`meta` property):
```markdown
---
title: Festival Digital Strategy Playbook
doc_type: sop
folder: playbooks/strategy
tags: [festival, launch]
owner: Strategy
---
# Festival Digital Strategy Playbook
The canonical method, distilled across past strategies. See [[Proposal Playbook]].
```
### 5. Avoid duplicates on a re-run
Before proposing, check `company_knowledge_list` for an existing doc with the same
`source_ref` or an obviously-equivalent title. If one exists, fold the new thinking
into that doc's proposal rather than creating a second near-duplicate.
### 6. Hand off to the human
When done, tell the user plainly: how many docs you proposed and the folders you
filed them under, that they should **review in the Knowledge → Map view and
publish** (nothing reaches their agents until they do), and anything you dropped
as asset/noise or were unsure how to file.
## Guardrails
- **Grow the brain; don't silo it.** Deliverables become shared, citable playbooks
in the corpus. Never route reusable know-how into a per-client Project to "keep
it separate" — that buries the company's intelligence. When unsure, it's brain.
- **Never publish.** `company_knowledge_propose` only. Publishing is a human,
owner/admin action.
- **Curate, don't mirror.** Don't propose one doc per file, and don't preserve the
dump's folder structure just because it was theirs. Merge across sources into
playbooks, distill, drop noise, re-derive a cleaner taxonomy.
- **No secrets or per-customer facts in Company Knowledge.** Credentials, API keys,
and one-customer working details (a specific client's budget/contacts) never
belong in the org corpus — strip them when distilling a deliverable into a
playbook.
- **Reuse the taxonomy** (step 3). Inventing ad-hoc folders/tags per file recreates
the mess you're here to fix.
- **Ceiling:** this is a one-shot / periodic *curate* pass. Keeping a large,
constantly-changing Drive continuously in sync is a different mechanism
(scheduled connector → markdown sync), not this skill. If the user wants
always-fresh mirroring of a big source, say so and stop — don't turn this into
a whole-Drive re-crawler.
Release notes
First public release (1.0.0 was submitted but not published): the Clawnify connector for company knowledge, projects, memory, tasks, agents and apps, and four skills for building on Clawnify and organizing company knowledge. Changed since the 1.0.0 submission, on the connected server: listing tasks and searching company knowledge no longer write anything, so both are read-only; replacing an agent's instructions is now marked open-world, because the agent goes on to answer people outside the organization and to run scheduled work; and ChatGPT no longer writes, publishes or runs flows, though it can still look up flows and their runs.
Declared in the saved package. Remote tools may change independently.
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package license
- MIT
- Package author
- Clawnify
- Keywords
- See keywords
- Commerce declaration
- Does not support commerceThis does not establish whether access is free or paid.
- Review scenarios
- 5 positive · 3 negativeScenario counts, not test results.
Declared capabilities
- Search company knowledge
- Review AI agent sessions
- Manage apps and their data
- Shared memory and tasks
Package observed Nov 5, 2026.
Technical details
- First seen
- Nov 5, 2026 · 00:00 UTC
- Last seen
- Nov 5, 2026 · 06:00 UTC
- Collection status
- Collected
plugin_asdk_app_6abebbc07294819182ee61dfb4b9f56f
Download plugin data (JSON)