Skip to content

Commit 33fa679

Browse files
feat(jobs): add Jobs.getById method [PLT-100298] (#358)
* feat(jobs): add Jobs.getByKey method [PLT-100298] Exposes the existing GET_BY_KEY endpoint as a public getByKey() method on the Jobs service. Refactors getOutput() to use getByKey() internally instead of the private fetchJobByKey() helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(jobs): rename getByKey to getById [PLT-100298] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: address comments * fix: address claude comments * fix: address comments * chore: address comments * fix: address comments * chore: address claude comments * chore: fix comments --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 83b95fb commit 33fa679

12 files changed

Lines changed: 348 additions & 73 deletions

File tree

.claude/skills/onboard-api/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Use the appropriate checklist based on the ticket type detected in Step 1. You m
7171
- Swagger/OpenAPI URL + endpoint paths → use directly
7272
- Missing either → stop and ask
7373

74-
**If Jira ticket:** Parse description for URLs ending in `.json`/`.yaml`/`swagger.json`/`openapi.json`, HTTP method + path patterns, scope strings (e.g., `OR.Jobs`). If Swagger URL missing, stop and report.
74+
**If Jira ticket:** Parse description for URLs ending in `.json`/`.yaml`/`swagger.json`/`openapi.json`, HTTP method + path patterns, scope strings (e.g., `OR.Jobs`), and whether each endpoint requires a `folderId` or `folderKey`. If Swagger URL missing, stop and report.
7575

7676
**Ticket type detection:**
7777
- `API:` field present → **Direct API** (existing workflow)
@@ -88,7 +88,7 @@ Log detected type in the summary.
8888
3. Check if the requested work is **already implemented**. If the work is already done (even on a feature branch), **stop and tell the user** instead of adding unrequested work.
8989
4. If the work is already implemented and the ticket is effectively complete, ask the user what they'd like done instead of inventing new work.
9090

91-
**Log summary**, then create feature branch:
91+
**Log summary** — present all collected input (Swagger URL, endpoints, OAuth scope, ticket type, folderId requirements) to the user. If any information is missing — including whether endpoints require a `folderId` — ask the user in this summary before creating the branch. Then create feature branch:
9292
- Jira: `feat/sdk-<ticket-key-lowered>`
9393
- Direct: `feat/<service>-<method-name>`
9494
- Already on feature branch → skip, note it

agent_docs/conventions.md

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
- **Use "Options" not "Request"** for parameter types — the entire SDK uses `{Entity}{Operation}Options`.
3434
- **Required parameters are always positional; Options objects are reserved for optional parameters only.** Required values (IDs, keys, data) are positional arguments. Options objects are always the last parameter, always marked `?`, and contain only optional fields. E.g., `getOutput(jobKey: string)` not `getOutput(options: { jobKey: string })`, `close(instanceId, folderKey, options?)` not `close(options: { instanceId, folderKey })`.
3535
- **NEVER** duplicate fields across option types — extend existing ones. If `CaseInstanceOperationOptions` already has `comment`, extend it instead of re-declaring. When the shape is identical, use `extends` (e.g., `export interface EntityUpdateRecordByIdOptions extends EntityGetRecordByIdOptions {}`).
36-
- **NEVER** use type aliases for response types — even when the shape matches an existing one, use an `extends` interface. Type aliases (e.g., `export type EntityUpdateRecordResponse = EntityRecord`) break TypeDoc docs generation by not rendering as standalone types. Use `export interface EntityUpdateRecordResponse extends EntityRecord {}` instead.
36+
- **Always use `type` for response types** (intersections, unions, composed types). The only place `interface extends` is required is single-type aliases (`type X = Y`), which break TypeDoc — use `export interface EntityUpdateRecordResponse extends EntityRecord {}` instead.
37+
38+
**ID parameter types**: New methods must use `string` (GUID) for entity identifiers, not `string | number`. Legacy methods may still accept `string | number` for backward compatibility, but all new `getById`, `getOutput`, etc. should type their ID parameter as `string`.
3739

3840
Method names: **singular** for single-item ops (`insertRecordById`), **plural** for batch (`insertRecordsById`). **NEVER** use `batch` prefix — the SDK convention is singular/plural to distinguish cardinality.
3941

@@ -159,6 +161,15 @@ OData APIs require `$` prefix on query params. The SDK accepts clean camelCase k
159161

160162
**Apply manually in:** `getById()` methods accepting `BaseOptions``const apiOptions = addPrefixToKeys(options, ODATA_PREFIX, Object.keys(options))`.
161163

164+
### Case for OData query values
165+
166+
OData is case-insensitive, so the server accepts either case. Convention: `filter`, `select`, `expand`, `orderby` values use the field case shown in the SDK response (camelCase for services that transform, else as-is) — not the raw API. Applies within JSDoc examples, tests, and internal calls.
167+
168+
```ts
169+
filter: "state eq 'Running'" // not "State eq ..."
170+
select: "outputArguments,outputFile" // not "OutputArguments,..."
171+
```
172+
162173
## Headers utility
163174

164175
`createHeaders()` from `src/utils/http/headers.ts` builds headers from key-value pairs, filtering undefined.
@@ -197,26 +208,12 @@ If the constructor only calls `super()` with no additional setup, omit it entire
197208

198209
## Folder-scoped services
199210

200-
Some Orchestrator services (Assets, Queues, Buckets) require a `folderId` for operations. These services handle it inline:
211+
Some Orchestrator services (Assets, Queues, Buckets, Jobs) require a `folderId` for operations. These services handle it inline:
201212

202213
- **`getById(id, folderId, ...)`** — sets the `X-UIPATH-OrganizationUnitId` header via `createHeaders({ [FOLDER_ID]: folderId })`
203214
- **`getAll(options?)`** — passes `getByFolderEndpoint` to `PaginationHelpers.getAll()`, which switches endpoints based on whether `folderId` is in options
204215

205-
### Required vs optional folderId
206-
207-
**Required folderId** (Assets, Queues, Buckets): `folderId` is a positional parameter — `getById(id, folderId, options?)`. The API requires folder scoping.
208-
209-
**Optional folderId** (Jobs): `folderId` is in the options object — `getById(id, options?)`. The API works across folders without a header.
210-
211-
**In both cases, use the same `createHeaders` call** — the utility filters `undefined` values automatically, so there's no need for conditional creation:
212-
213-
```typescript
214-
// CORRECT — consistent with all services in the codebase. createHeaders filters undefined.
215-
const headers = createHeaders({ [FOLDER_ID]: folderId });
216-
217-
// WRONG — unnecessary conditional. Breaks codebase consistency.
218-
const headers = folderId ? createHeaders({ [FOLDER_ID]: folderId }) : undefined;
219-
```
216+
Always pass `folderId` directly to `createHeaders` — the utility filters `undefined` values, so no conditional is needed.
220217

221218
## OperationResponse pattern
222219

agent_docs/rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
Every new method must also have an integration test in `tests/integration/shared/{domain}/`. These run against a live API and catch issues unit tests miss — wrong endpoints, broken transforms, auth/header problems.
2626

27-
- Use `console.warn()` + skip (not `throw`) for `beforeAll` setup preconditions that are outside the test's control (e.g., no test data available). `throw` is for test body guards where missing config means the test can't run at all. **NEVER** use `console.log` + `return` for integration test guards — silent skips hide missing test configuration.
2827
- Use `getServices()` and `getTestConfig()` from `tests/integration/config/unified-setup.ts`
2928
- Use `registerResource()` from `tests/integration/utils/cleanup.ts` for cleanup tracking
3029
- Use `generateRandomString()` from `tests/integration/utils/helpers.ts` for unique test data
@@ -48,6 +47,7 @@ JSDoc comments in `src/models/{domain}/*.models.ts` are the **source of truth fo
4847
- Use `<paramName>` placeholder convention for IDs in examples.
4948
- Use camelCase in examples, matching SDK response format. **NEVER** use PascalCase in JSDoc examples — users will write broken code.
5049
- Keep JSDoc in sync with method names.
50+
- **Keep JSDoc on service class methods in sync with `{Entity}ServiceModel`** — both the models file and the service implementation file must have identical JSDoc for each public method. `ServiceModel` is the source of truth for docs, but the service class copy aids developer navigation and IDE tooltips. When updating JSDoc on one, update the other.
5151
- **When a method supports `expand`**, show multiple expandable entities in the `@example` (e.g., `expand: 'Robot,Machine,Release'`) so users see the comma-separated pattern.
5252
- **Add a one-line description of what the response includes** beyond the method signature (e.g., "Returns the full job details including state, timing, and input/output arguments. Use `expand` to include related entities like Robot, Machine, or Release").
5353
- **NEVER** reference unrelated parameters in JSDoc examples — keep examples focused on the method being documented. If `getOutput()` doesn't accept `folderId`, don't show `folderId` in its example.

docs/oauth-scopes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This page lists the specific OAuth scopes required in external app for each SDK
1414
| Method | OAuth Scope |
1515
|--------|-------------|
1616
| `getAll()` | `OR.Jobs` or `OR.Jobs.Read` |
17+
| `getById()` | `OR.Jobs` or `OR.Jobs.Read` |
1718
| `getOutput()` | `OR.Jobs` or `OR.Jobs.Read` |
1819

1920
## Attachments

src/models/orchestrator/jobs.internal-types.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/models/orchestrator/jobs.models.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { JobGetAllOptions, RawJobGetResponse } from './jobs.types';
1+
import { JobGetAllOptions, JobGetByIdOptions, RawJobGetResponse } from './jobs.types';
22
import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination';
33

4-
// Combined type for job data with methods
4+
/** Combined response type for job data with bound methods. */
55
export type JobGetResponse = RawJobGetResponse & JobMethods;
66

77
/**
@@ -67,6 +67,35 @@ export interface JobServiceModel {
6767
: NonPaginatedResponse<JobGetResponse>
6868
>;
6969

70+
/**
71+
* Gets a job by its unique key (GUID).
72+
*
73+
* Returns the full job details including state, timing, input/output arguments, and error information.
74+
* Use `expand` to include related entities like `robot`, or `machine`.
75+
*
76+
* @param id - The unique key (GUID) of the job to retrieve
77+
* @param folderId - The folder ID where the job resides
78+
* @param options - Optional query options for expanding or selecting fields
79+
* @returns Promise resolving to a {@link JobGetResponse} with full job details and bound methods
80+
*
81+
* @example
82+
* ```typescript
83+
* // Get a job by key
84+
* const job = await jobs.getById(<id>, <folderId>);
85+
* console.log(job.state, job.processName);
86+
* ```
87+
*
88+
* @example
89+
* ```typescript
90+
* // With expanded related entities
91+
* const job = await jobs.getById(<id>, <folderId>, {
92+
* expand: 'robot,machine'
93+
* });
94+
* console.log(job.robot?.name, job.machine?.name);
95+
* ```
96+
*/
97+
getById(id: string, folderId: number, options?: JobGetByIdOptions): Promise<JobGetResponse>;
98+
7099
/**
71100
* Gets the output of a completed job.
72101
*

src/models/orchestrator/jobs.types.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { JobState, RequestOptions } from '../common/types';
1+
import { JobState, BaseOptions, RequestOptions } from '../common/types';
22
import { PaginationOptions } from '../../utils/pagination';
33
import {
44
JobPriority,
@@ -41,7 +41,7 @@ export enum ServerlessJobType {
4141
/**
4242
* Interface for process metadata associated with a job.
4343
* Represents a lightweight summary of the process (release) linked to a job.
44-
* Available when using 'expand: "Release"' in the query.
44+
* Available when using 'expand: "release"' in the query.
4545
*/
4646
export interface ProcessMetadata {
4747
/** The unique key of the release */
@@ -146,11 +146,11 @@ export interface RawJobGetResponse extends FolderProperties {
146146
parentSpanId: string | null;
147147
/** The error code associated with a failed job */
148148
errorCode: string | null;
149-
/** The machine associated with the job (available when using expand=Machine) */
149+
/** The machine associated with the job (available when using expand=machine) */
150150
machine?: Machine;
151-
/** The robot associated with the job (available when using expand=Robot) */
151+
/** The robot associated with the job (available when using expand=robot) */
152152
robot?: RobotMetadata;
153-
/** The process metadata associated with the job (available when using expand=Release) */
153+
/** The process metadata associated with the job */
154154
process?: ProcessMetadata | null;
155155
/** Error details for the job, or null if the job has no errors */
156156
jobError: JobError | null;
@@ -166,3 +166,8 @@ export type JobGetAllOptions = RequestOptions & PaginationOptions & {
166166
folderId?: number;
167167
}
168168

169+
/**
170+
* Options for getting a job by ID
171+
*/
172+
export interface JobGetByIdOptions extends BaseOptions {}
173+

src/services/orchestrator/jobs/jobs.ts

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { FolderScopedService } from '../../folder-scoped';
2-
import { RawJobGetResponse, JobGetAllOptions } from '../../../models/orchestrator/jobs.types';
3-
import { RawJobOutputFields } from '../../../models/orchestrator/jobs.internal-types';
2+
import { RawJobGetResponse, JobGetAllOptions, JobGetByIdOptions } from '../../../models/orchestrator/jobs.types';
43
import { JobServiceModel, JobGetResponse, createJobWithMethods } from '../../../models/orchestrator/jobs.models';
5-
import { pascalToCamelCaseKeys, transformData } from '../../../utils/transform';
4+
import { addPrefixToKeys, pascalToCamelCaseKeys, transformData } from '../../../utils/transform';
65
import { JOB_ENDPOINTS } from '../../../utils/constants/endpoints';
7-
import { ODATA_PAGINATION, ODATA_OFFSET_PARAMS } from '../../../utils/constants/common';
6+
import { ODATA_PAGINATION, ODATA_OFFSET_PARAMS, ODATA_PREFIX } from '../../../utils/constants/common';
87
import { JobMap } from '../../../models/orchestrator/jobs.constants';
98
import { AttachmentService } from '../attachments/attachments';
109
import { ValidationError, ServerError } from '../../../core/errors';
@@ -105,6 +104,58 @@ export class JobService extends FolderScopedService implements JobServiceModel {
105104
}, options) as any;
106105
}
107106

107+
/**
108+
* Gets a job by its unique key (GUID).
109+
*
110+
* Returns the full job details including state, timing, input/output arguments, and error information.
111+
* Use `expand` to include related entities like `robot`, or `machine`.
112+
*
113+
* @param id - The unique key (GUID) of the job to retrieve
114+
* @param folderId - The folder ID where the job resides
115+
* @param options - Optional query options for expanding or selecting fields
116+
* @returns Promise resolving to a {@link JobGetResponse} with full job details and bound methods
117+
*
118+
* @example
119+
* ```typescript
120+
* // Get a job by key
121+
* const job = await jobs.getById(<id>, <folderId>);
122+
* console.log(job.state, job.processName);
123+
* ```
124+
*
125+
* @example
126+
* ```typescript
127+
* // With expanded related entities
128+
* const job = await jobs.getById(<id>, <folderId>, {
129+
* expand: 'robot,machine'
130+
* });
131+
* console.log(job.robot?.name, job.machine?.name);
132+
* ```
133+
*/
134+
@track('Jobs.GetById')
135+
async getById(id: string, folderId: number, options?: JobGetByIdOptions): Promise<JobGetResponse> {
136+
if (!id) {
137+
throw new ValidationError({ message: 'id is required for getById' });
138+
}
139+
if (!folderId) {
140+
throw new ValidationError({ message: 'folderId is required for getById' });
141+
}
142+
143+
const headers = createHeaders({ [FOLDER_ID]: folderId });
144+
const keysToPrefix = Object.keys(options ?? {});
145+
const apiOptions = options ? addPrefixToKeys(options, ODATA_PREFIX, keysToPrefix) : {};
146+
147+
const response = await this.get<Record<string, unknown>>(
148+
JOB_ENDPOINTS.GET_BY_KEY(id),
149+
{
150+
params: apiOptions,
151+
headers,
152+
}
153+
);
154+
155+
const rawJob = transformData(pascalToCamelCaseKeys(response.data) as RawJobGetResponse, JobMap);
156+
return createJobWithMethods(rawJob, this);
157+
}
158+
108159
/**
109160
* Gets the output of a completed job.
110161
*
@@ -143,44 +194,23 @@ export class JobService extends FolderScopedService implements JobServiceModel {
143194
throw new ValidationError({ message: 'jobKey is required for getOutput' });
144195
}
145196

146-
const job = await this.fetchJobByKey(jobKey, folderId);
197+
const job = await this.getById(jobKey, folderId, { select: 'outputArguments,outputFile' });
147198

148-
if (job.OutputArguments) {
199+
if (job.outputArguments) {
149200
try {
150-
return JSON.parse(job.OutputArguments) as Record<string, unknown>;
201+
return JSON.parse(job.outputArguments) as Record<string, unknown>;
151202
} catch {
152203
throw new ServerError({ message: 'Failed to parse job output arguments as JSON' });
153204
}
154205
}
155206

156-
if (job.OutputFile) {
157-
return this.downloadOutputFile(job.OutputFile);
207+
if (job.outputFile) {
208+
return this.downloadOutputFile(job.outputFile);
158209
}
159210

160211
return null;
161212
}
162213

163-
/**
164-
* Fetches a job by its Key (GUID) using the GetByKey endpoint.
165-
* Only selects fields needed for output extraction.
166-
*/
167-
private async fetchJobByKey(
168-
jobKey: string,
169-
folderId: number
170-
): Promise<RawJobOutputFields> {
171-
const headers = createHeaders({ [FOLDER_ID]: folderId });
172-
const response = await this.get<RawJobOutputFields>(
173-
JOB_ENDPOINTS.GET_BY_KEY(jobKey),
174-
{
175-
params: {
176-
$select: 'OutputArguments,OutputFile',
177-
},
178-
headers,
179-
}
180-
);
181-
return response.data;
182-
}
183-
184214
/**
185215
* Downloads the output file content via the Attachments API.
186216
* 1. Fetches blob access info from the attachment using AttachmentService

0 commit comments

Comments
 (0)