Skip to content

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);
});
sdk.register('issue.created', version: 'v1') do |payload, context|
  project_id = payload['project_id']
  custom_data = context[:endpoints].custom_data(project_id)

  custom_data.create(
    'com.example.crm.contacts',
    'automation_records',
    email: 'lead@example.com',
    metadata: { source: 'webhook' }
  )
end