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_actiongenerated 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 withid,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_idmust reference an employee in your organization.typemust bestrengthoropportunity.observationis required and 1–4000 characters.impactandrecommended_actionare optional and capped at 2000 characters each.attribute_idsmust reference attributes in your organization. Up to 12 unique IDs.observation_datemust 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_idcannot be changed. Create a new observation if the subject is wrong.typecannot 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:
- Looks up the target employee by email or Slack handle.
- Calls
POST /observationswith the typed payload. - 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.
Generating recommended actions later
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.
Related
- API overview — pagination, filtering, idempotency.
- Authentication and API keys — scoping observation keys.
- Webhooks — real-time observation events.
- Summaries API — including observation IDs in a summary.