Developers

Observations API

Reference for listing, creating, updating, archiving, and restoring observations via the Performance Blocks API.

Plan availability: The Performance Blocks API is available on the Agentic plan. Team plan customers can upgrade in Settings → Billing.

Observations are the smallest unit of feedback in Performance Blocks. Each one captures a single strength or opportunity tied to one employee, optionally with impact, a recommended action, and tags (attributes). The Observations API lets you read, create, update, archive, and restore observations programmatically.

This is useful for:

  • Capturing observations from a custom intake tool — a Slack slash command, a mobile app, a 1:1 note template.
  • Importing historical feedback from another performance tool during a migration.
  • Enriching observations from other systems (e.g. attaching a recommended_action generated by an internal LLM).

Resource shape

{
  "id": "obs_01HXX9YK7Z8N2P3Q4R5S6T7U8V",
  "employee_id": "emp_01HXX...",
  "type": "strength",
  "observation": "Led the Q2 launch retro and surfaced two systemic gaps in how we plan releases.",
  "impact": "Directly informed the new release-readiness checklist that ships next sprint.",
  "recommended_action": "Document the retro framework so other leads can run it.",
  "attribute_ids": ["attr_01HXX...", "attr_01HYY..."],
  "observation_date": "2026-04-12",
  "created_at": "2026-04-12T15:42:18.000Z",
  "updated_at": "2026-04-12T15:42:18.000Z",
  "archived_at": null
}

Field reference

Field Type Notes
id string ULID prefixed with obs_. Read-only.
employee_id string The subject of the observation. Must reference an active or deactivated employee in your organization.
type enum "strength" or "opportunity". Required on create.
observation string The core description. Required. 1–4000 characters. Plain text; no HTML.
impact string | null Optional. 0–2000 characters. The downstream effect of the behavior.
recommended_action string | null Optional. 0–2000 characters. What to do next.
attribute_ids string[] Optional. Up to 12 attribute IDs from your organization's attribute library.
observation_date string (date) ISO 8601 date (YYYY-MM-DD). Defaults to today (UTC) if omitted on create.
created_at string (datetime) ISO 8601. Read-only.
updated_at string (datetime) ISO 8601. Read-only.
archived_at string (datetime) | null Set when the observation is archived. null for active observations. Read-only.

Expand paths

?expand= accepts:

  • employee — inlines the full Employee object.
  • attributes — inlines the array of Attribute objects (each with id, name, category, tone).

Multiple paths are comma-separated (?expand=employee,attributes).

List observations

GET /observations

Query parameters

Parameter Type Notes
limit integer Default 50, max 100.
cursor string From a previous next_cursor.
sort string One of created_at, -created_at, observation_date, -observation_date. Default -observation_date.
filter[employee_id] string Restrict to one employee.
filter[manager_id] string Observations of any direct or indirect report of this manager.
filter[type] enum strength or opportunity.
filter[archived] boolean false (default), true, or all.
filter[attribute_id] string Tagged with this attribute. Repeat for multiple (matches any).
filter[observation_date][gte] date Inclusive lower bound.
filter[observation_date][lte] date Inclusive upper bound.
filter[created_at][gte] datetime Inclusive lower bound.
filter[created_at][lte] datetime Inclusive upper bound.
expand string See above.

Example: curl

curl "https://api.performanceblocks.com/v1/observations\
?filter[employee_id]=emp_01HXX...\
&filter[observation_date][gte]=2026-01-01\
&limit=25" \
  -H "Authorization: Bearer $PB_API_KEY"

Example: TypeScript

const params = new URLSearchParams({
  'filter[employee_id]': 'emp_01HXX...',
  'filter[observation_date][gte]': '2026-01-01',
  limit: '25'
});

const res = await fetch(
  `https://api.performanceblocks.com/v1/observations? 

Example response

{
  "data": [
    {
      "id": "obs_01HXX...",
      "employee_id": "emp_01HXX...",
      "type": "strength",
      "observation": "Led the Q2 launch retro...",
      "impact": "Directly informed...",
      "recommended_action": null,
      "attribute_ids": ["attr_01HXX..."],
      "observation_date": "2026-04-12",
      "created_at": "2026-04-12T15:42:18.000Z",
      "updated_at": "2026-04-12T15:42:18.000Z",
      "archived_at": null
    }
  ],
  "next_cursor": "eyJpZCI6Im9ic18wMUhYWC4uLiJ9",
  "has_more": true
}

Get an observation

GET /observations/{id}

Returns a single observation. Returns 404 if the observation does not exist or is in another organization. Archived observations are returned without a special filter — check archived_at.

curl https://api.performanceblocks.com/v1/observations/obs_01HXX... \
  -H "Authorization: Bearer $PB_API_KEY"

Create an observation

POST /observations

Required scope: observations:write.

Request body

{
  "employee_id": "emp_01HXX...",
  "type": "opportunity",
  "observation": "The Q2 launch retro stayed too high-level. Missed the chance to nail down owners.",
  "impact": "Two of the action items have already slipped a sprint.",
  "recommended_action": "Use the action-item template and assign owners before closing the retro.",
  "attribute_ids": ["attr_01HXX..."],
  "observation_date": "2026-04-12"
}

Validation rules

  • employee_id must reference an employee in your organization.
  • type must be strength or opportunity.
  • observation is required and 1–4000 characters.
  • impact and recommended_action are optional and capped at 2000 characters each.
  • attribute_ids must reference attributes in your organization. Up to 12 unique IDs.
  • observation_date must be a valid ISO 8601 date (YYYY-MM-DD) and not in the future.

A successful create returns 201 with the new resource.

Example: TypeScript

const idempotencyKey = crypto.randomUUID();

const res = await fetch('https://api.performanceblocks.com/v1/observations', {
  method: 'POST',
  headers: {
    Authorization: `Bearer  

Always include an Idempotency-Key on creates. If the request times out and you retry, the API will return the original observation rather than creating a duplicate.

Update an observation

PATCH /observations/{id}

Required scope: observations:write.

PATCH is a partial update — only send the fields you want to change. To clear an optional string field, send null. To clear attribute_ids, send an empty array.

Request body (any subset of)

{
  "observation": "Refined wording after a 1:1.",
  "impact": "Updated impact statement.",
  "recommended_action": null,
  "attribute_ids": ["attr_01HXX..."]
}

Restrictions

  • employee_id cannot be changed. Create a new observation if the subject is wrong.
  • type cannot be changed. Create a new observation and archive the old one.
  • Archived observations cannot be updated. Restore first, then update.

Example: curl

curl -X PATCH https://api.performanceblocks.com/v1/observations/obs_01HXX... \
  -H "Authorization: Bearer $PB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"recommended_action": null}'

Archive an observation

POST /observations/{id}/archive

Required scope: observations:write.

Archiving sets archived_at to the current time. The observation is excluded from default lists, summaries, and search results, but it is preserved for audit.

curl -X POST https://api.performanceblocks.com/v1/observations/obs_01HXX.../archive \
  -H "Authorization: Bearer $PB_API_KEY"

Returns 200 with the archived observation, or 409 conflict if it was already archived.

Restore an observation

POST /observations/{id}/restore

Required scope: observations:write.

Clears archived_at. The observation reappears in default lists.

curl -X POST https://api.performanceblocks.com/v1/observations/obs_01HXX.../restore \
  -H "Authorization: Bearer $PB_API_KEY"

Returns 200 or 409 conflict if the observation was not archived.

Webhook events

If you have a webhooks:write subscription with the relevant event types enabled, you'll receive a delivery for each of these:

Event Fires when
observation.created A new observation is created (via API or app).
observation.updated Any field on an active observation is updated.
observation.archived An observation transitions to archived.
observation.restored An archived observation is restored.

The delivery data field contains the full observation resource (post-change). See Webhooks for delivery semantics, signature verification, and retry behavior.

Common error responses

Status Code Cause
400 validation_error Missing required field, value out of range, malformed date.
401 authentication_required Missing or invalid API key.
403 permission_denied Key lacks observations:write (or :read for GETs).
404 not_found Observation ID doesn't exist or is in another organization.
409 conflict Archive on already-archived, restore on non-archived, or idempotency key mismatch.
422 unprocessable Referenced employee or attribute is not in your organization.
429 rate_limited Throttled. See Retry-After.

A typical validation error:

{
  "error": {
    "code": "validation_error",
    "message": "1 field failed validation",
    "details": [
      { "field": "type", "code": "invalid_enum", "message": "must be one of: strength, opportunity" }
    ]
  }
}

Use cases

Importing from another tool

When migrating from another performance tool, batch-create observations in chronological order. Map the source type to strength or opportunity, set observation_date to the original capture date (not today), and use a stable Idempotency-Key derived from the source ID so you can re-run the import safely.

const idempotencyKey = `import-source: 

Capturing from a custom Slack command

Wire /feedback to a small backend that:

  1. Looks up the target employee by email or Slack handle.
  2. Calls POST /observations with the typed payload.
  3. Replies in-channel with a confirmation and a link to the observation in the application.

Use a Slack-Trigger-ID-derived idempotency key so duplicate event deliveries from Slack don't create duplicate observations.

Create the observation immediately with just observation and type. A separate job (cron, queue worker, or webhook on observation.created) calls your LLM, generates a recommended_action, and PATCHes the observation when the suggestion is ready. Users see the observation immediately and the action appears as soon as it's generated.

© 2026 Performance Blocks. All rights reserved.