Extension Custom Data¶
Extensions can declare and access custom data modules the same way workspaces do, with .kiket/modules/<module>/schema.yml files and manifest-defined permissions. This guide covers the manifest contract, the new extension API endpoints, the CLI helpers, and the Python SDK additions.
Declare Permissions in the Manifest¶
Add a custom_data.permissions block to your .kiket/manifest.yaml to enumerate the modules you need and the allowed operations.
extension:
id: com.example.crm
name: CRM Toolkit
custom_data:
permissions:
- module: com.example.crm.contacts
operations: [read, write]
- module: kiket.team.capacity
operations: [read]
Operations:
read– list and retrieve records.write– create/update/destroy (soft delete) records.admin– destructive operations such as hard deletes or schema resets.
Modules shipped inside the extension package live under .kiket/modules/<module>/schema.yml. They are namespaced automatically per installation (installation:<id>) so the manifest always references the logical module ID (e.g., com.example.crm.contacts).
Extension API Endpoints¶
The extension API exposes custom data CRUD endpoints. All requests require a runtime token (X-Kiket-Runtime-Token) and the custom_data.read or custom_data.write scope.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/ext/custom_data/:module/:table |
List records |
| GET | /api/v1/ext/custom_data/:module/:table/:id |
Fetch single record |
| POST | /api/v1/ext/custom_data/:module/:table |
Create a record |
| PATCH | /api/v1/ext/custom_data/:module/:table/:id |
Update a record |
| DELETE | /api/v1/ext/custom_data/:module/:table/:id |
Soft delete (or hard delete) |
Example – list records:
curl -H "X-Kiket-Runtime-Token: rt_abc123..." \
"https://kiket.dev/api/v1/ext/custom_data/com.example.crm.contacts/automation_records?project_id=42&limit=50"
Response:
{
"data": [
{
"id": 1,
"email": "contact@example.com",
"metadata": {"source": "api"},
"organization_id": 7,
"project_id": 42,
"created_at": "2025-11-08T12:34:17Z",
"updated_at": "2025-11-08T12:34:17Z"
}
]
}
Create:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Kiket-Runtime-Token: rt_abc123..." \
-d '{"project_id":42,"record":{"email":"new@example.com","metadata":{"source":"api"}}}' \
"https://kiket.dev/api/v1/ext/custom_data/com.example.crm.contacts/automation_records"
CLI Helpers¶
The CLI exposes convenience commands that speak to the same API endpoints:
# List the first 25 contact records (requires a workspace token via `kiket auth login`)
kiket extensions custom-data:list com.example.crm.contacts automation_records \
--project 42 \
--limit 25 \
--filters '{"status":"open"}'
# Create or update data using a project key
kiket extensions custom-data:create com.example.crm.contacts automation_records \
--project-key CRM \
--record '{"email":"lead@example.com","metadata":{"source":"demo"}}'
The custom data commands accept --filters (JSON) for list operations and --record payloads for create/update. Authenticate once with kiket auth login (workspace API token) and supply either --project (ID) or --project-key when running the commands.
SDK Usage¶
All SDKs expose a custom_data(project_id) helper through context.endpoints (or equivalent). The SDK automatically uses the runtime token from webhook payloads to authenticate API calls.
@sdk.webhook("issue.created", version="2025-11-01")
async def issue_created(payload, ctx):
project_id = payload["project_id"]
contacts = await ctx.endpoints.custom_data(project_id).list(
"com.example.crm.contacts",
"automation_records",
limit=10,
filters={"status": "active"},
)
await ctx.endpoints.custom_data(project_id).create(
"com.example.crm.contacts",
"automation_records",
{"email": "new@example.com"},
)
sdk.webhook('issue.created', 'v1')(async (payload, context) => {
const projectId = payload.issue.project_id;
const records = await context.endpoints.customData(projectId).list(
'com.example.crm.contacts',
'automation_records',
{ limit: 25, filters: { status: 'active' } }
);
await context.endpoints.customData(projectId).update(
'com.example.crm.contacts',
'automation_records',
records.data[0].id,
{ status: 'nurturing' }
);
});
sdk.Register("issue.created", "v1", async (payload, context) =>
{
var projectId = payload["project_id"]!.ToString();
var customData = context.Endpoints.CustomData(projectId!);
await customData.CreateAsync(
"com.example.crm.contacts",
"automation_records",
new Dictionary<string, object>
{
["email"] = "lead@example.com",
["metadata"] = new Dictionary<string, object> { ["source"] = "webhook" }
});
});
sdk.register("issue.created", "v1", (payload, context) -> {
String projectId = String.valueOf(((Map<?, ?>) payload.get("project_id")));
var customData = context.getEndpoints().customData(projectId);
customData.delete("com.example.crm.contacts", "automation_records", "123");
return Map.of("ok", true);
});