Pulse Solution Services
Overview
@skedulo/pulse-solution-services provides common services and utilities for building solutions on the Pulse platform.
Installation
npm install @skedulo/pulse-solution-services
or
yarn add @skedulo/pulse-solution-services
Quick Start
Initialize Execution Context
From a function
const context = ExecutionContext.fromContext(skedContext, {
requestSource: "my-cool-project",
userAgent: "my-cool-function"
});
The requestSource
and userAgent
options are important for observability and debugging, as they set the value for the X-Skedulo-Source
and User-Agent
headers that are automatically included in all requests made within this context.
From other execution contexts
const context = ExecutionContext.fromCredentials({
apiServer: user.apiBasePath,
apiToken: user.accessToken,
}, {
requestSource: "my-cool-project",
userAgent: "my-cool-function"
});
Once initialized, the context object provides access to the various services detailed in subsequent sections.
Execution Limits
Overview
Execution Limits serve two main purposes:
- Provide execution metrics to help developers optimize their code.
- Ensure system stability by terminating executions that exceed limits. While this is not currently the default behavior, it may be in the future.
By default, the following limit are set:
- Total API Calls: 50
- Max Concurrent Requests: 5
- Max Method Execution Time: 12 seconds
A warning is logged if a limit is violated.
Example
Using default limits
const context = ExecutionContext.fromContext(skedContext, {
requestSource: "my-cool-project",
userAgent: "my-cool-function"
});
const artifactClient = context.newArtifactClient(ArtifactType.WEBHOOK);
const webhooks = await artifactClient.list();
const functionPromises = webhooks.map((webhook: any) => artifactClient.get(webhook.name));
await Promise.all(functionPromises);
console.log(context.getExecutionMetrics());
Overriding limits
const limits = {
totalCalls: 50,
maxConcurrentRequests: 10,
maxMethodExecutionTimeMs: 10000,
throwOnViolation: true
};
const context = ExecutionContext.fromContext(skedContext, {
requestSource: "my-cool-project",
userAgent: "my-cool-function",
limits
});
Config Helper
Retrieves the values of configuration variables defined within a function. To use this helper, the execution context must be initialized from skedContext
.
const secretValue = context.configHelper.getString('SECRET_NAME');
In addition to getString()
, other available methods include getNumber()
and getJson()
.
API Clients
Base API Client
Handles requests to any Skedulo API that does not have a dedicated API client implementation.
GET request
context.baseClient.performRequest({
endpoint: `files/avatar?user_ids=<id1>,<id2>`,
});
POST request
context.baseClient.performRequest({
method: "POST",
endpoint: `function/hello-func/hello-func/ping`,
body: { message: "Hello" }
});
Metadata Client
Fetches metadata from /metadata
endpoints.
const metadata = await context.metadataClient.fetchAllMetadata();
const jobMetadata = await context.metadataClient.fetchObjectMetadata('job');
Vocabulary Client
Manages vocabulary items for picklist fields in schemas.
const vocabularyItems = await context.vocabularyClient.getVocabularyItems('Jobs', 'Type');
const newVocabularyItem = await context.vocabularyClient.addVocabularyItem('Jobs', 'Type', {
value: "Repair",
label: "Repair Service",
active: true,
defaultValue: false,
description: "A repair job type"
});
const updatedVocabularyItem = await context.vocabularyClient.updateVocabularyItem('Jobs', 'Type', 'Upgrade', {
value: "Upgrade",
label: "System Upgrade",
description: "An upgraded system installation"
});
GraphQL Client
Executes GraphQL queries and mutations against /graphql/graphql
.
const queryResult = await context.graphqlClient.execute(queryString, {readOnly: true});
const graphQLRequest: GraphQLRequest = {
query: queryString,
variables: {}
}
const queryUsingVariablesResult = await context.graphqlClient.execute(graphQLRequest, {readOnly: true});
Config Variable Client
Provides CRUD and search methods for managing configuration variables.
await context.configVarClient.create({
key: "TEST_KEY",
value: "TEST_VALUE",
configType: "plain-text",
description: "TEST_DESCRIPTION",
});
const config = await context.configVarClient.get('TEST_KEY');
const config = await context.configVarClient.delete('TEST_KEY');
Org Preferences Client
Fetch Org Preferences from the /config/org_preference
endpoint.
const config: Record<string, any> = await context.orgPreferencesClient.get();
const jobConfig = {
allowAbortJob:false
}
const config: Record<string, any> = await context.orgPreferencesClient.deploy(jobConfig);
Config Features (Feature Flags) Client
Fetch Config Features from the /config/features
endpoint.
const config: Record<string, any> = await context.configFeaturesClient.get();
Mobile Notification Client
Manage mobile notifications, SMS messaging, and templates.
Mobile Notification Templates
const templates = await context.mobileNotificationClient.getTemplates();
await context.mobileNotificationClient.setTemplate(
NotificationTemplate.JOB_DISPATCH,
NotificationType.SMS,
'Updated text for job dispatch: {{ Name }} starts at {{ Start }}.'
);
await context.mobileNotificationClient.deleteTemplate(NotificationTemplate.JOB_DISPATCH, NotificationType.SMS);
Job Notifications
Send notifications related to job dispatch, reminders, cancellations, and custom messages.
const result: DispatchResponse = await context.mobileNotificationClient.dispatchResources({
jobId: "12345",
resourceIds: ["resource-1", "resource-2"]
});
const result: NotifyResponse = await context.mobileNotificationClient.notifyAllocatedResources({
jobId: "12345",
resourceIds: ["resource-1", "resource-2"]
});
const result: NotifyCancelResponse = await context.mobileNotificationClient.notifyJobCancellation({
jobId: "12345"
});
const result: OneOffResponse = await context.mobileNotificationClient.sendOneOffMessage({
resourceId: "resource-1",
message: "Reminder: Your job starts in 1 hour.",
protocol: NotificationType.PUSH
});
SMS Messaging
Send SMS messages or request job confirmations via SMS.
const result: SmsResponse = await context.mobileNotificationClient.sendSms({
phoneNumber: "+1234567890",
countryCode: "US",
message: "Thank you for confirming your appointment!",
expectsReply: false
});
const result: SmsResponse = await context.mobileNotificationClient.requestSmsConfirmation({
phoneNumber: "+1234567890",
countryCode: "US",
jobId: "12345",
message: "Please confirm your job by replying YES or NO."
});
Configuration Templates
Templates
const templates: ConfigTemplate[] = await context.configTemplateClient.get('Jobs');
const values: ConfigValue[] = await context.configTemplateClient.getTemplateValues(templateId);
const result: Record<string,string> = await context.configTemplateClient.delete(templateId);
Template Values
const values: ConfigValue[] = await context.configTemplateClient.getTemplateValues(templateId);
const newTemplate: ConfigTemplate = {
name: "Installation",
schemaName: 'Jobs'
}
const createdTemplate: ConfigTemplate = await context.configTemplateClient.create(newTemplate);
console.log(JSON.stringify(createdTemplate, null, 2));
const configTemplateValues: ConfigValue[] = [
{
templateId: createdTemplate.id!,
rel: "job",
field: "CanBeDeclined",
value: "true"
},
{
templateId: createdTemplate.id!,
rel: "job",
field: "Duration",
value: "120"
}
]
const newValues = await context.configTemplateClient.upsertTemplateValues(createdTemplate.id!, configTemplateValues);
console.log(JSON.stringify(newValues, null, 2));
Geo API Client
Provides access to geolocation-related services via the /geoservices
endpoints.
const distanceMatrix: DistanceMatrixResponse = await context.geoAPIClient.getDistanceMatrix({
origins: [{ lat: 37.7749, lng: -122.4194 }],
destinations: [{ lat: 34.0522, lng: -118.2437 }],
avoid: ["highway"],
});
const directions: DirectionsResponse = await context.geoAPIClient.getDirections({
requests: [{
origin: { lat: 37.7749, lng: -122.4194 },
destination: { lat: 34.0522, lng: -118.2437 },
waypoints: [{ lat: 36.7783, lng: -119.4179 }],
avoid: ["toll"],
}],
});
const geocode: GeocodeResponse = await context.geoAPIClient.geocodeAddress({
addresses: ["1600 Amphitheatre Parkway, Mountain View, CA"],
});
const autocomplete = await context.geoAPIClient.autocompleteAddress({
input: "1600 Amphi",
sessionId: "unique-session-id",
location: { lat: 37.7749, lng: -122.4194 },
radius: 5000,
country: "US",
});
const placeDetails = await context.geoAPIClient.getPlaceDetails({
placeId: "ChIJ2eUgeAK6j4ARbn5u_wAGqWA",
sessionId: "unique-session-id",
});
const timezone: TimezoneResponse = await context.geoAPIClient.getTimezone({
location: [37.7749, -122.4194],
timestamp: Math.floor(Date.now() / 1000),
});
Availability API Client
Fetch Resource Availability
const resourceAvailability: AvailabilityResult[] = await context.availabilityAPIClient.fetchAvailability({
resourceIds: ["resource_123", "resource_456"],
start: "2024-06-01T00:00:00Z",
end: "2024-06-02T00:00:00Z",
mergedAvailabilities: false,
entries: true
});
console.log(availability[0].mergedAvailabilities);
console.log(availability[0].entries);
Upsert Availability Patterns
const patternRequest = {
pattern: {
name: "Availability Pattern - MON",
pattern: {
type: "weekly" as const,
repeatWeeks: 1,
days: [
{
weekday: "MON" as const,
intervals: [{ startTime: "08:00", endTime: "18:00" }]
}
]
}
},
resources: [
{
resourceId: "<resource_id>",
start: "2025-08-31T14:00:00.000Z",
end: "2025-09-28T13:59:59.999Z"
}
]
};
const patternId = await context.availabilityAPIClient.upsertPattern(patternRequest);
Files API Client
Get avatars
const params: GetAvatarParams = {
userIds: ["<user1>", "<user2>"],
sizeHint: "small",
};
const result = await context.filesAPIClient.getAvatar(params);
Get attachments
const attachments = await context.filesAPIClient.getAttachments(parentId);
Artifact Client
The ArtifactClient
provides a unified way to manage various artifact types.
const client = context.newArtifactClient(ArtifactType.CUSTOM_OBJECT);
Listing All Artifacts
Retrieve all artifactse.
await client.list();
Using Artifact Parameters for CRUD Operations
When performing CRUD operations, provide ArtifactParams
with the necessary values based on the artifact type.
type ArtifactParams = {
name?: string;
objectName?: string;
viewTypeName?: string;
scope?: string;
};
Notes
- Use the object label for
objectName
in Horizon artifacts.
name
is the slug
for Horizon templates and defId
for mobile extensions.
await client.get({ name: "Jobs" });
const upsertStatus: any = await client.upsert({},
{
metadata: {
type: 'CustomObject'
},
name: 'Tasks',
label: 'Tasks',
description: 'Daily tasks'
}
);
await new Promise(resolve => setTimeout(resolve, 2000));
const result = await client.status(upsertStatus.id);
await client.upsert(
{
name: 'Tasks'
},
{
metadata: {
type: 'CustomObject'
},
name: 'Tasks',
label: 'Tasks',
description: 'Daily tasks - updated'
}
);
await client.delete(
{
name: 'Tasks'
}
);
GraphQL Service
Query data
const queryBuilder = context.newQueryBuilder({
objectName: 'Jobs',
operationName: 'fetchJobWithJobAllocations',
readOnly: true
});
queryBuilder.withFields(['UID', 'Name'])
.withFilter("JobStatus != 'Complete'")
.withLimit(1)
.withOffset(1)
.withOrderBy('Name ASC');
queryBuilder.withChildQuery('JobAllocations')
.withFields(['UID', 'Name'])
.withParentQuery('Job')
.withFields(['UID', 'Name']);
queryBuilder.withParentQuery('Region')
.withFields(['UID', 'Name']);
let queryResult = await queryBuilder.execute();
console.log(queryResult.records);
console.log(queryResult.totalCount);
console.log(queryResult.queryLimit);
console.log(queryResult.pageInfo.hasNextPage);
console.log(queryResult.endCursor);
console.log(queryResult.endOffset);
The X-Graphql-Operation
header is added to all GraphQL requests and contains the value of operationName
.
Query Builder Methods
The GraphQLQueryBuilder
provides several methods to configure your queries:
Limiting Results:
withLimit(limit: number)
- Sets the maximum number of records using the limit
parameter (recommended)
withFirst(first: number)
- Sets the maximum number of records using the first
parameter (use when first
is specifically needed)
queryBuilder.withLimit(100);
queryBuilder.withFirst(100);
Filtering and Ordering:
withFields(fields: string[])
- Specify which fields to retrieve
withFilter(condition: string)
- Add filter conditions to limit results
withOrderBy(orderBy: string)
- Set the sort order (e.g., 'Name ASC'
, 'CreatedDate DESC'
)
Pagination:
withOffset(offset: number)
- Set the starting position for results
withCursor(after: string)
- Use cursor-based pagination
Query by Pages
const queryBuilder = context.newQueryBuilder({
objectName: 'Jobs',
operationName: 'fetchJobs',
readOnly: true
});
const queryResult = await queryBuilder.executeAll(1, 5);
const queryResult = await queryBuilder.executeAll();
The queryBuilder.executeAll
method supports querying up to a maximum of 20 pages.
Batch Queries
To execute multiple queries in a single request, use graphqlService.queryBatch
. This method takes an array of query builders, sends them to the GraphQL batch endpoint, and returns an array of query results. This is useful for fetching data from multiple objects efficiently.
const jobQueryBuilder = context.newQueryBuilder({
objectName: 'Jobs',
operationName: 'fetchJobs',
readOnly: true
});
jobQueryBuilder.withFields(['UID', 'Name'])
.withLimit(10);
const contactQueryBuilder = context.newQueryBuilder({
objectName: 'Contacts',
operationName: 'fetchContacts',
readOnly: true
});
contactQueryBuilder.withFields(['UID', 'FirstName', 'LastName'])
.withLimit(5);
const queryResults = await context.graphqlService.queryBatch([jobQueryBuilder, contactQueryBuilder]);
const jobResults = queryResults[0];
const contactResults = queryResults[1];
console.log('Jobs:', jobResults.records);
console.log('Job Total Count:', jobResults.totalCount);
console.log('Contacts:', contactResults.records);
console.log('Contact Total Count:', contactResults.totalCount);
Update data
const recordsToUpdate = queryResult.records.map((record) => {
return {
UID: record.UID,
Name: record.Name + " - Updated"
}
});
const updateResult = await context.graphqlService.update({
objectName: 'Resources',
operationName: 'updateResources',
records: recordsToUpdate,
bulkOperation: true,
suppressChangeEvents: true
});
upsert data
const upsertResult = await context.graphqlService.upsert({
objectName: 'Resources',
operationName: 'updateResources',
records: recordsToUpsert,
bulkOperation: true,
suppressChangeEvents: true,
keyField: "ExternalId"
});
Batch mutations
const insertJobParams = {
objectName: 'Jobs',
operationName: 'insertJobs',
operation: GraphqlOperations.INSERT,
records: [
{ UID: 'job1', Name: 'Developer Job', Status: 'Open' },
{ UID: 'job2', Name: 'Designer Job', Status: 'Open' }
],
bulkOperation: true
};
const deleteContactParams = {
objectName: 'Contacts',
operationName: 'deleteContacts',
operation: GraphqlOperations.DELETE,
records: [
{ UID: 'contact1' },
{ UID: 'contact2' }
]
};
const mutationResults = await context.graphqlService.mutateBatch([insertJobParams, deleteContactParams]);
const insertResult = mutationResults[0];
const deleteResult = mutationResults[1];
console.log('Inserted Job UIDs:', context.graphqlService.extractUIDs(insertResult));
console.log('Deleted Contact UIDs:', context.graphqlService.extractUIDs(deleteResult));
The bulkOperation
and readOnly
options help optimize large-scale mutations and read-heavy queries. When enabled, they automatically add the X-Skedulo-Bulk-Operation
(for mutations) and X-Skedulo-Read-Only
(for queries) headers to improve performance.
To reduce the impact on change event processing, especially during high-volume mutations, the suppressChangeEvents
option can be used. This disables change history tracking, helping to minimize delays.
The GraphQL Service supports pagination using either offset-based or cursor-based strategies, allowing you to fetch large datasets incrementally.
Pagination with offset
while(queryResult.pageInfo.hasNextPage) {
queryBuilder.withOffset(queryResult.endOffset + 1);
queryResult = await queryBuilder.execute();
}
Pagination with cursor
while(queryResult.pageInfo.hasNextPage) {
queryBuilder.withCursor(queryResult.endCursor);
queryResult = await queryBuilder.execute();
}
Resource Service
The Resource Service consists of two main components:
- Resource Builder: Retrieves resource information, including availability, scheduled jobs, activities, tags, etc
- Resource Validator: Validates which resources are available for which jobs based on given conditions.
Resource Builder
The Resource Builder retrieves resource data, including availability, activities, scheduled jobs, tags, and other relevant details. By default, it enables the following options: useTag
, useJobAllocation
, useResourceShift
, useActivity
, useAvailabilityPattern
, and useHoliday
, with a time range set to 14 days from the current time.
Usage
const params = new ResourceQueryParam();
params.regionIds = new Set(['<region_id>']);
params.useTag = false;
params.startTime = new Date();
params.endTime = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000);
const resources = await context.resourceBuilder.build(params);
console.log(resources);
Resource Validator
The Resource Validator takes a list of resources and jobs, and determines which resources are qualified to perform the jobs based on:
- Availability: Is the resource available during the job's timeframe?
- Conflict Detection: Does the resource already have conflicting events (e.g., another job, activity, or time-off)?
- Tag Matching: Does the resource have the required skills for the job?
Usage
const jobQueryResult = await context
.newQueryBuilder({ objectName: "Jobs", operationName: "fetchJobs", readOnly: true })
.withLimit(2)
.withFields(TenantObjects.Jobs.Fields)
.execute();
const jobs: Job[] = jobQueryResult.records.map((job) => EntityFactory.createJob(job));
const validationOptions = {
checkAvailability: true,
checkConflict: true,
checkTag: true
};
const validator = new ResourceValidator(validationOptions);
const result: ValidationResult = validator.validate(resources, jobs);
console.log(JSON.stringify(result, null, 2));
const resourcesQualifiedForAtleastOneJob = result.getQualifiedResources();
const jobsWithAtLeastXQualifiedResources = result.getJobsWithXQualifiedResources(2);
const unqualifiedJobsWithReasons = result.getUnqualifiedJobsWithReasons();
Validation Output
The Resource Validator returns a structured JSON output indicating which resources are qualified, and the reasons why certain jobs are not assigned. The results can be formatted to present a clearer qualification status along with detailed reasons.
console.log(JSON.stringify(result.format(), null, 2));
GeoService
Provides a high-level interface for geolocation-related operations, built on top of GeoAPIClient
.
Get Distance Matrix
Computes travel distance and duration between origins and destinations. Handles cases where no route is available.
const origins = [{ lat: 55.93, lng: -3.118 }, { lat: 50.087, lng: 14.421 }];
const destinations = [{ lat: 50.087, lng: 14.421 }, { lat: 0, lng: 0 }];
const distanceMatrix: Map<string, DistanceMatrixEntry> = await context.geoService.getDistanceMatrix(origins, destinations);
for(const origin of origins) {
for(const destination of destinations) {
const key = context.geoService.createKey(origin, destination);
const entry = distanceMatrix.get(key);
console.log(`Origin-Destination: ${key} - Status: ${entry?.status}`);
if(entry?.status === "OK") {
console.log(`Distance: ${entry?.distance?.distanceInMeters} meters`);
console.log(`Duration: ${entry?.duration?.durationInSeconds} seconds`);
}
}
}
Get Address Suggestions
Fetches address autocomplete suggestions and retrieves detailed place information.
const input = "1600 Amphitheatre Pkwy";
const suggestions: PlaceResponse[]|null = await context.geoService.getAddressSuggestions({ input, country: 'US' }, 2);
if (suggestions) {
suggestions.forEach((place, index) => {
console.log(`Place ${index + 1}: ${place.formattedAddress}`);
console.log(`Latitude: ${place.geometry.lat}, Longitude: ${place.geometry.lng}`);
});
} else {
console.log("No place details found.");
}
Lock Service
The Lock Service provides a locking mechanism to prevent concurrent execution of critical processes, such as batch jobs. It ensures that only one instance of a process can proceed at a time by using configuration variables with expiration timestamps.
const lockAcquired = await context.lockService.acquireLock({
name: 'MY_LOCK',
ttl: 5 * 60 * 1000,
});
await context.lockService.releaseLock('MY_LOCK');
Batch Service
The Batch Service offers a flexible and scalable solution for large-scale data processing.
How to Use
- Create a subclass of GraphBatch that encapsulates the specific logic for your batch job.
- Override the
start
method to return a configured GraphQLQueryBuilder object. This builder should set up the query fields, filters, and any required ordering.
- Override the
execute
method to implement the logic to process each batch of records (e.g., updates, deletes, logging).
- Optionally override the
finish
method to perform any final cleanup, such as logging the results or sending notifications.
- Run the batch.
Example
Below is an example of bulk job update.
import {
GraphBatch,
GraphqlOperations,
GraphQLQueryBuilder,
} from "@skedulo/pulse-solution-services";
export class BulkJobUpdate extends GraphBatch {
public result: string[] = [];
protected async start(): Promise<GraphQLQueryBuilder> {
const queryBuilder = this.context
.newQueryBuilder({objectName: "Jobs", operationName: "fetchJobsWithRegion"})
.withFields(["UID", "Name", "Start"])
.withOrderBy("Name ASC");
queryBuilder.withParentQuery("Region").withFields(["UID", "Name"]);
return queryBuilder;
}
protected async execute(records: any[]): Promise<void> {
const recordsToUpdate = records.map((record) => ({
UID: record.UID,
Description: `${record.Name} scheduled for ${new Date(record.Start).toLocaleDateString("en-GB")} in ${record.Region.Name}`,
}));
await this.context.graphqlService.mutate({
objectName: "Jobs",
operation: GraphqlOperations.UPDATE,
operationName: "updateJobs",
records: recordsToUpdate
});
this.result.push(...records.map((record) => record.Name));
}
protected async finish(): Promise<void> {
this.context.logger.info(`Processed ${this.result.length} records.`);
}
}
Run the batch
const context = ExecutionContext.fromCredentials({
apiServer: user.apiBasePath,
apiToken: user.user.accessToken,
});
const batch = new BulkJobUpdate(context, {
batchSize: 10,
maxBatches: 10,
strategy: PaginationStrategy.CURSOR,
delaySeconds: 1,
});
await batch.run();
Unique Batch
The Unique Batch ensures that only one instance runs at a time. It leverages the Lock Service to prevent concurrent executions. The default lock duration is 16 minutes, configurable via the lockTtl
setting.
Implementation
To create a unique batch, extend the UniqueGraphBatch
class, everything else remains the same as a regular GraphBatch
.
export class UniqueBulkJobUpdate extends UniqueGraphBatch {
Run a unique batch
const batch = new UniqueBulkJobUpdate(context, {
batchSize: 10,
maxBatches: 10,
strategy: PaginationStrategy.CURSOR,
delaySeconds: 1,
lockTtl: 5 * 60 * 1000,
});
batch.run();
batch.run();
Cache Service
The Cache Service enables efficient data sharing between function executions and improves performance by reducing redundant operations and API calls.
const metadata: object[] = await context.inMemoryCache.get(
"METADATA",{
useSecondaryCache: true,
}
)
Logging Utils
The log method decorator helps streamline debugging by tracking key function executions, including their inputs and outputs.
class ExampleService {
@LogMethod("example_space")
processData(input: string): string {
return `Processed: ${input}`;
}
}
Two environment variables control logging behavior:
- LOG_NAMESPACE: Filters logs by a specific namespace or displays all logs. The namespace for services in this library is PSS.
- LOG_ENTRY_MAX_LENGTH: Defaults to 120 but can be increased to capture complete input/output data.