Skip to main content
Version: 2026.3

How to Run a Background Job

Overview​

Start a long-running operation from your plugin, show it in the running-jobs panel with progress, retry and error details, and keep it there across page reloads.

Details​

Pimcore Studio runs long operations through the Generic Execution Engine. The frontend never polls your endpoint: the backend publishes progress and the terminal state over Mercure to the user's private topic, and the execution-engine module renders those updates in the jobs panel. Both sides have to follow a small contract.

Backend​

  • Start the job with JobExecutionAgentInterface::startJobExecution($job, $userId, Config::CONTEXT_CONTINUE_ON_ERROR->value) (or CONTEXT_STOP_ON_ERROR). Only runs in one of these two Studio execution contexts, owned by the current user, are listed by the running-jobs endpoint and can be rehydrated after a reload.
  • Return the run id from your controller as 201 { "jobRunId": <int> }.
  • Publish progress from your step handlers with HandlerProgressTrait::updateProgress().
  • Add a JobRunStateChangedEvent subscriber filtered on your job name that publishes Finished to UserTopicServiceInterface::getUserTopic($ownerId) for FINISHED, and calls EventSubscriberServiceInterface::handleFinishedWithErrors() for FINISHED_WITH_ERRORS. FAILED is already published by Studio's FailureSubscriber for the two Studio contexts.

Frontend​

  • Implement JobInterface. Declare static readonly jobNames with your backend job name(s) and static rehydrate(jobRuns); both run() and rehydrate() build the same MessageBusJobHandler.
  • Register the class in your module's onInit() on the JobRehydrationRegistry service. ExecutionEngine.runJob() throws for a job whose names are not registered.
  • Run it with useExecutionEngine().runJob(new MyJob(...)). runJob() resolves once the request is dispatched and the handler registered, not when the job ends; put completion side effects in onJobCompletion.
  • Pass StepCompletionCalculator as progressCalculator when each backend step completes as a unit. For any other progress shape implement ProgressCalculator; calculateProgress() returns a number (0–100), null (indeterminate) or PROGRESS_NO_UPDATE (leave the bar as is).
import { t } from 'i18next'
import { store } from '@pimcore/studio-ui-bundle/app'
import {
MessageBusJobHandler,
StepCompletionCalculator,
type JobCompletionData,
type JobInterface,
type JobRunList,
type JobRunOptions,
type RehydratableJob
} from '@pimcore/studio-ui-bundle/modules/execution-engine'
import { api } from '../my-api-slice-enhanced'

export class MyJob implements JobInterface {
static readonly jobNames = ['my_bundle_job'] as const

static rehydrate ([jobRun]: JobRunList): MessageBusJobHandler {
return MyJob.buildHandler({ jobRunId: jobRun.id })
}

private static buildHandler (options: { jobRunId: number, onRetry?: () => Promise<void> }): MessageBusJobHandler {
return new MessageBusJobHandler({
jobRunId: options.jobRunId,
title: t('my-bundle.job-title'),
progressCalculator: new StepCompletionCalculator(),
onRetry: options.onRetry,
onJobCompletion: async (data: JobCompletionData): Promise<void> => {
if (data.isFinished) {
store.dispatch(api.util.invalidateTags(['MyData']))
}
}
})
}

async run (options: JobRunOptions): Promise<void> {
const response = await store.dispatch(api.endpoints.myBundleStartJob.initiate({}))
const jobRunId = response.data?.jobRunId

if (jobRunId === undefined) {
return
}

options.messageBus.registerHandler(MyJob.buildHandler({
jobRunId,
onRetry: async () => { await this.run(options) }
}))
}
}

void (MyJob satisfies RehydratableJob)
// in your module's onInit()
import { container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type JobRehydrationRegistry } from '@pimcore/studio-ui-bundle/modules/execution-engine'

container
.get<JobRehydrationRegistry>(serviceIds['ExecutionEngine/JobRehydrationRegistry'])
.register(MyJob)

Code Example on GitHub​

A complete in-repo example is the bulk import job: bulk-import-job.ts, registered in bulk-import/index.ts.