Runlog docs

Start building

A plan buys a weekly allowance, sized for steady work.

Start

Schemas

For data that has a shape before it arrives. A record schema declares the typed records a project holds beside its files; a type's schema declares the fields every entity of that type is read into.

Records

Not everything in a project is a file. A named object is a typed record — a form submission, a contact, a product, an owner-curated list — declared by a schema and held against it. Schemas and the submit action are reachable today; record CRUD over the API is specified and lands with the rest of the surface.

GET/v1/artifact-schemasAPI key

List schemas

Read the typed schemas the project's artifacts and records are held against.

A schema is what makes a record queryable rather than a blob: it declares the fields, their types, and which are required.

Request · curl
curl -X GET "https://runlog-7613480744.us-central1.run.app/v1/artifact-schemas" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
{
  "schemas": [
    {
      "fields": [
        {
          "name": "palette",
          "type": "string[]"
        }
      ],
      "name": "Design mood board",
      "schemaId": "sch_4Kd2Qm"
    }
  ]
}

Errors

StatuserrorWhen
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
POST/v1/artifact-schemasAPI key

Declare a schema

Create or update a typed schema the project's records are held against.

Body

FieldTypeRequiredDescription
namestringrequiredDisplay name for the schema.
fieldsobject[]requiredField declarations: name, type, and whether it is required.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/artifact-schemas" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "fields": [
    {
      "name": "email",
      "required": true,
      "type": "string"
    },
    {
      "name": "square_feet",
      "type": "number"
    }
  ],
  "name": "Intake request"
}'
Response · 201
{
  "name": "Intake request",
  "schemaId": "sch_9Lm3Tz"
}

Errors

StatuserrorWhen
400invalid request bodyThe body is not JSON, or exceeds the 1 MB request-body cap.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
POST/v1/api/actionsAPI key

Submit a record

Write one typed record against the bound named-object type, validated on write.

The type is the owner's binding, never a field of the submission: a payload that does not fit the bound type's declared fields is refused, not stored.

Dispatched as `{"action": "object_submit", "input": {"data": {...}}}`.

Body

FieldTypeRequiredDescription
actionstringrequiredMust be `"object_submit"`.
input.dataobjectrequiredThe record's fields, keyed by the bound type's field keys.
input.honeypotstringoptionalLeave empty; a filled value marks the submission as spam.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/api/actions" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "object_submit",
  "input": {
    "data": {
      "email": "amber@example.com",
      "square_feet": 2400
    }
  }
}'
Response · 200
{
  "action": "object_submit",
  "result": {
    "recordId": "rec_2Xm9Qd"
  }
}

Errors

StatuserrorWhen
400record does not match its schemaA required field is absent, or a field's type disagrees with the declaration.
403action not available to an API keyThe key's tier does not reach this action.
400invalid request bodyThe body is not JSON, or exceeds the 1 MB request-body cap.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.

Types

Every entity type in a project is itself an entity carrying the type's schema: the typed fields each entity of that type is read into. An entity's `typeEntityId` names its type; editing the type's schema bumps its version, and every entity of the type is re-read against it the next time new material mentions it.

PUT/v1/projects/{projectId}/entities/{entityId}/schemaSession

Edit a type's schema

Replace a type's fields, against the version the edit started from; the schema moves one version up.

`version` is the version you read, `0` for a type that had none. If the schema moved since, the edit is refused with the current schema so you can start again from it.

An edited schema is yours from then on: ingestion never rewrites it, it only proposes changes for you to approve or reject.

A field's `type` is a word of the field vocabulary (`text`, `number`, `date` and the rest — a 400 lists them all). Field names are unique ignoring case.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
entityIdstringrequiredThe type entity carrying the schema.

Body

FieldTypeRequiredDescription
fieldsobject[]requiredThe complete field list: name, type, description, and whether it is required.
versionintegerrequiredThe schema version the edit started from.
Request · curl
curl -X PUT "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/entities/{entityId}/schema" \
  -H "Authorization: Bearer $RUNLOG_SESSION_TOKEN" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "fields": [
    {
      "description": "When payment is due.",
      "name": "dueDate",
      "required": true,
      "type": "date"
    }
  ],
  "version": 2
}'
Response · 200
{
  "categorySchema": {
    "fields": [
      {
        "description": "When payment is due.",
        "name": "dueDate",
        "required": true,
        "type": "date"
      }
    ],
    "version": 3
  }
}

Errors

StatuserrorWhen
409schema moved to version 3 since the edit beganSomeone else saved first; the body carries `categorySchema`, the schema to start again from.
400schema field "dueDate": names the same key as "Due Date"A field is unnamed, duplicated, reserved, too long, or of a type outside the vocabulary.
400entity is not a category; its schema lives on its type entityThe id names an entity of a type rather than the type.
404entity not foundThe project holds no such entity.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
400invalid request bodyThe body is not JSON, or exceeds the 1 MB request-body cap.
401unauthorizedNo valid session.
POST/v1/projects/{projectId}/entities/{entityId}/schema/approveSession

Approve a proposed change

Make the change ingestion proposed for a type its live schema, one version up.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
entityIdstringrequiredThe type entity carrying the schema.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/entities/{entityId}/schema/approve" \
  -H "Authorization: Bearer $RUNLOG_SESSION_TOKEN" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{
  "categorySchema": {
    "fields": [
      {
        "description": "When payment is due.",
        "name": "dueDate",
        "required": true,
        "type": "date"
      }
    ],
    "version": 3
  }
}

Errors

StatuserrorWhen
404no pending schema proposalThe type carries no proposed change.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401unauthorizedNo valid session.
POST/v1/projects/{projectId}/entities/{entityId}/schema/rejectSession

Reject a proposed change

Drop the change ingestion proposed for a type, keeping its schema.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
entityIdstringrequiredThe type entity carrying the schema.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/entities/{entityId}/schema/reject" \
  -H "Authorization: Bearer $RUNLOG_SESSION_TOKEN" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{}

Errors

StatuserrorWhen
404no pending schema proposalThe type carries no proposed change.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401unauthorizedNo valid session.

Proposed types

When the same record shape recurs across your files — a form, a table family, a message kind — ingestion proposes it as a type, citing where it was read. Nothing is bound to a proposal until you confirm it; a rejected shape is never proposed again.

GET/v1/projects/{projectId}/category-proposalsSession

List proposed types

Page the shapes awaiting your decision, newest first.

The body is a bare array; the next page's cursor rides on the `X-Next-Cursor` header, empty on the last page. `evidence` lists the sections the shape was read from.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe project to read.

Query parameters

FieldTypeRequiredDescription
cursorstringoptionalCursor from a previous page.
limitintegeroptionalPage size, capped at 200.
Request · curl
curl -X GET "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/category-proposals" \
  -H "Authorization: Bearer $RUNLOG_SESSION_TOKEN" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
[
  {
    "description": "A bill for goods or services.",
    "evidence": [
      "fa_3Qm8Xz"
    ],
    "fields": [
      {
        "description": "When payment is due.",
        "name": "dueDate",
        "required": true,
        "type": "date"
      }
    ],
    "name": "Invoice",
    "proposalId": "cp_3Xr9Lm",
    "status": 1,
    "updatedAt": "2026-08-07T18:22:10Z"
  }
]

Errors

StatuserrorWhen
400invalid cursorThe cursor is not one a previous page returned.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401unauthorizedNo valid session.
POST/v1/projects/{projectId}/category-proposals/{proposalId}/confirmSession

Confirm a proposed type

Make a shape a type: its fields become the type's schema, and the sections it was read from are filed under it.

Confirming a shape whose name is an existing type gives that type the fields. Confirming twice answers the same `entityId`.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
proposalIdstringrequiredThe proposal to settle.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/category-proposals/{proposalId}/confirm" \
  -H "Authorization: Bearer $RUNLOG_SESSION_TOKEN" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{
  "entityId": "t_8Qm2Kd"
}

Errors

StatuserrorWhen
404category proposal not foundThe project holds no such proposal.
409category proposal already settledIt was confirmed or rejected already the other way.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401unauthorizedNo valid session.
POST/v1/projects/{projectId}/category-proposals/{proposalId}/rejectSession

Reject a proposed type

Refuse a shape; it is never proposed again.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
proposalIdstringrequiredThe proposal to settle.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/category-proposals/{proposalId}/reject" \
  -H "Authorization: Bearer $RUNLOG_SESSION_TOKEN" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{}

Errors

StatuserrorWhen
404category proposal not foundThe project holds no such proposal.
409category proposal already settledIt was confirmed or rejected already the other way.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401unauthorizedNo valid session.