How to Read and Update Employee Goals from Any Performance Platform with Unified.to
August 13, 2026
Your product can read a customer's employee goals from whichever performance platform they run, through one connection and one method call.
sdk.performance.listPerformanceGoals returns the same object whether the customer is on 15Five, BambooHR, HiBob, or Lattice, and a webhook subscription keeps your copy of that list current as employees update goals in their own platform. Every request is executed directly against the customer's platform, so what you read is what exists there now.
Why goals are the hard part of a performance integration
Any product that plans compensation, prepares managers for check-ins, or reports on team progress needs to know what each employee is working toward and how far along they are. The customer has already answered that question, in detail, inside their performance platform. Reading their answer is the integration.
The difficulty is that each platform models goals in its own API, with its own shapes for hierarchy, ownership, and measurable progress. Building against each one means a separate integration, separate auth, and separate handling for pagination, rate limits, and errors, before you have written any of your own product. The Unified Performance Management API normalizes the concept into a single Goal object so you build the read once.
What you are building
A goals module inside your product: the customer connects their performance platform, their employees' goals appear with progress and milestones, and updates flow both ways. The list stays current without a nightly job.
- Authorize the customer's platform and store one connection ID
- List their goals
- Render hierarchy, ownership, and quantified progress from the fields their platform provides
- Create goals and write progress updates back
- Subscribe to changes so the module updates itself
Connect once and list every goal
Initialize the SDK with your API key. This is the same for every category.
import { UnifiedTo } from '@unified-api/typescript-sdk';
import * as errors from '@unified-api/typescript-sdk/sdk/models/errors';
const sdk = new UnifiedTo({
security: {
jwt: process.env.UNIFIED_API_KEY,
},
});
If your customers require their data to stay in a region, pass serverIdx when initializing: 0 for the North American server, 1 for European, 2 for Australian.
Each end-customer authorizes their own platform through Unified's authorization component, which returns a connection ID. Store one per customer. That ID is the only thing that changes between a customer on Lattice and a customer on BambooHR.
Listing their goals takes one call:
const goals = await sdk.performance.listPerformanceGoals({
connectionId,
limit: 50,
offset: 0,
sort: 'updated_at',
order: 'desc',
});
id and name are your floor. Every integration that supports the Goal object returns them, which is enough to render a working list on day one.
What each field tells you, and why it changes your interface
The rest of the Goal object is where a flat list becomes something a manager trusts. Six fields carry information most goals interfaces do not show, because most are built against one platform and inherit its assumptions.
progress returns a percentage from 0 to 100, and status returns where the goal sits in its lifecycle: not started, in progress, completed, or closed. Read them together. A goal at 90 percent that is closed and a goal at 90 percent that is in progress with a due date next week are different situations wearing the same number.
type returns INDIVIDUAL, TEAM, DEPARTMENT, or COMPANY, and it is also a list parameter, so you can request one level at a time. A company goal and an individual goal do not belong in the same flat list: one is context, the other is work. Filter on this rather than asking the customer to infer it from a goal's name.
parent_id is the id of the parent goal, which is how platforms represent goal alignment and OKR trees. It is also a list parameter: pass parent_id to read one subtree, which is how you render "everything under this company objective" without fetching and filtering the whole catalog.
user_ids returns the employees who own or share the goal, and the first element is the primary owner. These are references to the HRIS employee object, so if your product already reads employee data through Unified's HR & Directory category, a goal joins to the same identity with no second employee model and no name matching.
milestones returns the goal's milestones or key results, each with a target_value, current_value, unit, weight, and is_completed. This is the difference between showing "70% complete" and showing "$1.4M of $2M ARR" with the unit attached. Where the platform provides it, quantified progress is the most useful thing on the screen.
weight returns how much this goal counts when the customer's platform weighs goals in reviews. Where it returns, show it: two goals at the same progress are not equally important to the employee's review, and the customer's platform already knows which matters more.
Handling coverage that varies by platform
Each platform's own API determines what Unified can return, so a field that exists on the object may not be available on every integration. This is a first-class part of building on any category, and three habits handle it.
Check support before you scope. Field support, webhook support, and list parameters are documented per integration on the Supported Integrations page. If your feature depends on milestones, confirm which platforms return it before you promise it.
Treat 501 as information, not failure. Where a platform's API does not support an object, the request returns 501 Not Implemented rather than an empty list. An empty list means the customer has no goals; a 501 means this platform cannot tell you either way. Show those differently.
try {
const goals = await sdk.performance.listPerformanceGoals({ connectionId });
return renderGoals(goals);
} catch (error) {
if (error instanceof errors.UnifiedToError && error.statusCode === 501) {
return renderUnsupported();
}
throw error;
}
UnifiedToError is the base class for every HTTP error response and carries statusCode alongside the message, headers, and body, so the same pattern handles authorization failures and rate limiting next to the unsupported case.
Render what returns. Build the interface so name, progress, and status are sufficient, and hierarchy, milestones, and weight are enhancements that appear when the platform provides them. A module that degrades gracefully works across the whole category.
Create and update goals from your product
When a manager sets a goal in your product during a check-in, write it to the platform of record:
const created = await sdk.performance.createPerformanceGoal({
connectionId,
performanceGoal: {
name: 'Grow ARR to $2M',
type: 'INDIVIDUAL',
user_ids: [employeeId],
cycle_id: activeCycleId,
due_at: '2026-12-31T00:00:00Z',
milestones: [
{ name: 'Close Q4 pipeline', target_value: 2000000, unit: 'USD' },
],
},
});
The first entry in user_ids is the primary owner, and it references the same HRIS employee object your HR & Directory reads return, so the goal is attached to a real identity from the moment it exists.
Updates use the same shape with the goal's id. When an employee moves progress in your product:
const updated = await sdk.performance.updatePerformanceGoal({
connectionId,
id: goalId,
performanceGoal: {
progress: 75,
},
});
Both writes are executed directly against the customer's platform, so the goal their HR team sees in Lattice or BambooHR reflects the change when your request returns, not on a sync cycle. Which fields are writable on which integration is in the capability matrix; write support is narrower than read support across every HR category, because platforms guard writes more tightly than reads, so gate your create and edit interfaces on the platforms your customers actually run.
Stay current when a goal changes
An employee who updates a goal in their performance platform expects your module to agree with it. Rather than re-reading the full list on a schedule, subscribe to changes and let Unified detect them and deliver events to your endpoint.
{
"connection_id": "5de520f96e439b002043d8dc",
"hook_url": "https://yourapp.com/webhooks/performance",
"object_type": "performance_goal",
"event": "updated",
"interval": 5,
"webhook_type": "virtual"
}
Subscribe separately for created and updated. The interval field sets how often Unified checks for changes, and can be set as low as one minute on paid accounts and 60 minutes on free ones. Passing include_all on the create request sends your server all existing goals on the first run, which backfills without a separate import path.
Each subscription carries a read-only is_healthy field. Read it as part of your own monitoring, so a subscription that has stopped delivering surfaces in your product rather than in a support ticket.
Webhook coverage in the performance category is documented per integration and, as of August 2026, available on 15Five. Fall back to a scheduled read where a platform has no webhook events.
Read only what changed
For the platforms where you are reading on a schedule, updatedGte in the SDK, documented as updated_gte on the REST endpoint, returns only records modified since a timestamp you supply.
const changed = await sdk.performance.listPerformanceGoals({
connectionId,
updatedGte: lastSyncedAt,
sort: 'updated_at',
order: 'asc',
limit: 100,
});
Sorting ascending on updated_at means the last record in the response carries the timestamp for your next call, so you do not maintain a cursor. offset is zero-based on every integration regardless of how that platform pages internally, and you know you have reached the end when a response returns fewer records than the limit you asked for. Not every integration supports every list parameter, which is recorded on the Supported Integrations page.
→ Start your 30-day free trial → Book a demo
Frequently asked questions
Should I read goals at request time or synchronize them into my own database?
Both, for different jobs. Read at request time when you need the current state of a goal you can already identify, such as the one a manager has open in a check-in:
const goal = await sdk.performance.getPerformanceGoal({ connectionId, id: goalId });
Synchronize when you need to search, filter, or join across goals, because the query parameter searches on name, and anything beyond that has to happen in a database you control. The webhook subscription that keeps your module current is also what populates the copy you query against.
How do I render an OKR tree from a flat list of goals?
Every goal carries parent_id, so the tree is already in the data: goals with no parent are roots, and everything else attaches under its parent. To read one subtree on demand instead of building the whole tree, pass parent_id as a list parameter and Unified returns only that objective's children.
What happens on a platform that returns only a name and a progress number?
Your module still works. Build so name, progress, and status carry the interface, and treat milestones, weight, and hierarchy as enhancements that appear when the platform provides them. The Supported Integrations page tells you which platforms return which fields before you scope the feature.
Can I update goals on every platform?
Within the limits of each platform's API. Write support is narrower than read support everywhere in HR data, because performance platforms restrict external writes more tightly than reads. The capability matrix documents which fields are writable per integration, and it is the current source as coverage extends.
Author
Written by Mallory Greene
Mallory Greene is Head of Marketing at Unified.to. She writes about integration infrastructure, unified APIs, and MCP for technical teams. Based in Toronto.