Skip to content

Job Definitions ​

Job Definitions are resources that wrap components to make them schedulable. They define what code should run when a job is executed.

What are Job Definitions? ​

A Job Definition is a simple resource that associates a unique ID with a component. It acts as a template for creating jobs:

Component (code) → Job Definition (wrapper) → Job (scheduled instance)

Why Job Definitions? ​

Job Definitions provide a layer of indirection that enables:

  • Reusability: One component can have multiple job definitions
  • Organization: Group related scheduled tasks
  • Permissions: Control who can schedule which components
  • Statistics: Track performance by job definition

Structure ​

A Job Definition associates a unique ID with a component, and optionally declares when it should run:

  • jobDefinitionId - Unique identifier for the definition
  • componentId - Which component to execute
  • queue (optional) - Which worker queue the job runs on (see Queues and Concurrency)
  • maxConcurrency - Maximum number of this definition's jobs that may run concurrently (optional)
  • schedules - Declared cron schedules (optional; see Scheduling)
  • scheduleSuspended - Whether the schedule is paused (default false)
  • scheduleConcurrency - forbid or allow when an occurrence comes due while a previous run is still outstanding (default forbid)
  • scheduleStartingDeadlineSeconds - How late an occurrence may still start, in seconds (optional)
yaml
uri: /resources/job-definitions/nightly-order-processing
spec:
  componentId: process-orders.hreactor
  queue: priority
  maxConcurrency: 1
  schedules:
    - cron: '0 2 * * *'
      timeZone: Europe/Stockholm

Queues and Concurrency ​

Jobs run on worker queues. Two queues are available:

  • default - The standard queue. Used when queue is omitted.
  • priority - A separate queue for time-critical jobs that should not wait behind slower or bulk work.

Each queue has its own dedicated workers, so a backlog on one queue never delays jobs on the other. Put latency-sensitive jobs on priority and leave everything else on default.

INFO

Capacity is tied to your contract: The number of workers processing each queue is determined by your tenant plan, not configured per app. Queues partition the processing capacity available to your tenant between priority classes — they do not add capacity. If you need more throughput, contact us about your plan.

Limiting concurrency ​

Some jobs must not run in parallel — for example, a job that calls an external system with strict rate limits, or one whose work isn't safe to overlap. Use maxConcurrency to cap how many jobs of a definition run simultaneously:

yaml
uri: /resources/job-definitions/sync-external-inventory
spec:
  componentId: sync-inventory.hreactor
  maxConcurrency: 1
  • maxConcurrency: 1 forces strictly sequential execution for this definition.
  • When omitted, concurrency is unbounded (limited only by available workers).

maxConcurrency is evaluated against the definition's current value and applies regardless of which queue the job runs on. The two settings are independent: queue decides where a job runs, maxConcurrency decides how many run at once.

Complete Example ​

Here's how to create a scheduled background task:

  1. Create a component with the logic to execute:

    filtrera
    //process-orders.hreactor
    import 'resources'
    
    param processingDate: text
    
    let orders = query orders(orderNumber)
      filter $'createdAt >= {processingDate}'
      orderBy 'createdAt asc'
    
    from orders select order =>
      sendEmail {
        to = order.customer.email
        subject = 'Processing Update'
        body = { html = '<p>Your order is being processed</p>' }
        dynamic = { orderId = order.id }
      }
  2. Create a job definition that wraps the component:

    yaml
    uri: /resources/components/process-orders.hreactor
    spec:
      codeFile: process-orders.hreactor
    
    ---
    uri: /resources/job-definitions/nightly-order-processing
    spec:
      componentId: process-orders.hreactor
  3. Deploy the manifest:

    bash
    h_ manage apply h_manifest.yaml
  4. Schedule a job using the definition:

    http
    POST /resources/jobs
    {
      "jobDefinitionId": "nightly-order-processing",
      "parameters": {
        "processingDate": "2025-11-15T00:00:00Z"
      },
      "runAt": "2025-11-16T02:00:00Z"
    }

Now the component will run at 2 AM with the specified parameters.

One Component, Multiple Definitions ​

A single component can be wrapped by multiple job definitions for different purposes:

yaml
# Same component for different use cases
uri: /resources/job-definitions/hourly-sync
spec:
  componentId: data-sync.hreactor

---
uri: /resources/job-definitions/daily-full-sync
spec:
  componentId: data-sync.hreactor

---
uri: /resources/job-definitions/manual-sync
spec:
  componentId: data-sync.hreactor

This allows:

  • Different scheduling patterns (hourly vs daily)
  • Different monitoring/statistics tracking
  • Different permission scopes
  • Same underlying logic

Scheduling ​

A job definition can declare when it should run using standard cron expressions. The platform then materializes exactly one job per scheduled occurrence — reliably, and without any coordination between scheduler instances.

Declaring a schedule ​

yaml
uri: /resources/job-definitions/nightly-order-processing
spec:
  componentId: process-orders.hreactor
  schedules:
    - cron: '0 2 * * *'
      timeZone: Europe/Stockholm

Each entry is a standard 5-field cron expression — minute hour day-of-month month day-of-week — optionally interpreted in a named time zone:

  • cron - The expression. Seconds are not supported; one minute is the resolution floor.
  • timeZone - An IANA or Windows time-zone id (e.g. Europe/Stockholm). Omit for UTC.

A definition may declare multiple schedules, each with its own time zone. Overlapping entries are harmless — colliding occurrences collapse into a single job.

INFO

Cron dialect. The Jenkins-style jitter character H is rejected: it makes occurrence times non-deterministic, which would break exactly-once scheduling. Day-of-month and day-of-week use Vixie-cron AND semantics — 0 3 1 * 1 means "the 1st and Monday", not "the 1st or Monday". L, W and # are supported.

Concurrency ​

scheduleConcurrency decides what happens when an occurrence comes due while a previous run is still outstanding:

  • forbid (default) - Skip the occurrence. This is the only setting that bounds backlog for definitions without a maxConcurrency.
  • allow - Run anyway.

replace is deliberately not supported — running jobs cannot be preempted. Note that scheduleConcurrency is distinct from maxConcurrency: the former decides whether a job is created, the latter how many may execute.

Starting deadline ​

scheduleStartingDeadlineSeconds bounds how late an occurrence may still start. It exists to prevent an outage from becoming a thundering herd: after downtime, only the most recent occurrence inside the deadline is materialized — older ones are dropped, never backfilled.

For example, an hourly schedule with a 300-second deadline that misses eight hours of downtime produces one catch-up job, not eight.

Suspending a schedule ​

Set scheduleSuspended to true to pause a schedule without deleting it. This is the explicit, reversible way to stop a schedule — unlike cancelling a pending job, which is indistinguishable from the job having failed.

App-provided schedules and overrides ​

A job definition shipped by an app carries the author's default schedule. A tenant can override it per definition; the override is stored separately, so an app upgrade that changes its default does not silently discard the tenant's decision. The scheduleIsOverridden field on the definition indicates whether a tenant override is in effect.

Previewing occurrences ​

To see when a schedule will next fire, request the definition with an occurrences parameter:

http
GET /resources/job-definitions/nightly-order-processing?occurrences=5

The response includes an occurrences array with the next five occurrence times, so you can verify a cron expression before saving it.

Managing Job Definitions ​

Job definitions are managed via the /resources/job-definitions API:

  • Create/Update: PUT /resources/job-definitions/{definitionId} — full assert; an absent field is cleared
  • Update schedule: PATCH /resources/job-definitions/{**definitionId} — merge; an absent field is left unchanged, an explicit null clears it
  • Get: GET /resources/job-definitions/{definitionId} (add ?occurrences=N to preview upcoming runs)
  • List: GET /resources/job-definitions
  • Delete: DELETE /resources/job-definitions/{definitionId}

PATCH is the only verb that reaches app-provided definitions (ids prefixed with apps/…), whose schedule is stored as a tenant override rather than on the definition itself.

See the API Reference for complete endpoint documentation.

WARNING

Deletion Warning: If you delete a job definition while scheduled jobs for it still exist, those jobs will fail when they attempt to run. Consider waiting for pending jobs to complete before deleting definitions.

Job Definition vs Job ​

Job Definition:

  • Template/configuration
  • Points to a component
  • Permanent resource
  • One per scheduled task type

Job:

  • Scheduled instance
  • References a job definition
  • Temporary (deleted after retention period)
  • Many instances per definition

Example:

  • Job Definition: nightly-order-processing (permanent)
  • Jobs: Individual executions (Nov 15 2AM, Nov 16 2AM, Nov 17 2AM, etc.)

Access Control ​

Managing job definitions requires permissions:

job-definitions:read     # View job definitions
job-definitions:write    # Create, update, delete job definitions

Statistics & Monitoring ​

Job statistics are tracked by jobDefinitionId, making it easy to monitor performance of specific scheduled tasks:

http
GET /resources/jobs/statistics?jobDefinitionId=nightly-order-processing

This allows you to track:

  • Success/failure rates for this specific task
  • Execution time trends
  • Queue depth for this definition

See Jobs documentation for complete statistics information.

Self-Scheduling (Relative Time) ​

TIP

For a fixed cadence, prefer declarative scheduling — it is exactly-once, survives failed runs, and never drifts. Self-scheduling remains useful for per-entity timeouts, where the next run depends on a specific entity's state rather than a wall-clock schedule.

A component can schedule its own next run at the end of its execution, using relative time scheduling:

How It Works ​

A component schedules the next run at the end of its execution:

filtrera
//nightly-processor.hreactor

// Do the work
let orders = query orders(orderNumber)
  filter 'status = pending'

let processResult = orders select order => processOrder(order)

// Schedule next run (24 hours from now)
from {
  effect = 'scheduleJob'
  definition = 'nightly-order-processing'
  at = now addDays 1
  parameters = {}
}

This creates a self-perpetuating chain: Job completes → Schedules next run → Next job executes → Schedules another run → ...

INFO

Timing: The at time is relative to when the job completes, not when it starts. If a job takes 10 minutes to run and schedules itself with now addHours 1, the next run is 1 hour and 10 minutes from the previous start.

Common Patterns ​

Hourly Processing ​

filtrera
// At end of component
from {
  effect = 'scheduleJob'
  definition = 'hourly-sync'
  at = now addHours 1
  parameters = {}
}

Daily Processing ​

filtrera
// At end of component
from {
  effect = 'scheduleJob'
  definition = 'daily-report'
  at = now addDays 1
  parameters = {}
}

Weekly Processing ​

filtrera
// At end of component
from {
  effect = 'scheduleJob'
  definition = 'weekly-cleanup'
  at = now addDays 7
  parameters = {}
}

Custom Intervals ​

filtrera
// Every 30 minutes
from {
  effect = 'scheduleJob'
  definition = 'frequent-sync'
  at = now addMinutes 30
  parameters = {}
}

Starting the Recurring Chain ​

Create the first job manually via API or rule to start the chain:

http
POST /resources/jobs
{
  "jobDefinitionId": "nightly-order-processing",
  "parameters": {},
  "runAt": "2025-11-16T02:00:00Z"
}

After this first execution, the job schedules itself automatically.

Stopping Recurring Jobs ​

To stop a recurring job chain, cancel the next pending job:

http
DELETE /resources/jobs/{jobId}

When a job is cancelled, it never executes, so it never schedules the next job. The chain breaks automatically.

Alternative methods:

  • Update the component to remove self-scheduling logic (permanent change)
  • Delete the job definition (will fail any pending jobs)

For a declarative schedule, stop it by setting scheduleSuspended to true instead — see Scheduling.

Best Practices ​

Use Descriptive IDs ​

Choose clear job definition IDs that describe purpose and frequency:

  • ✅ hourly-inventory-sync, daily-report-generation, weekly-cleanup
  • ❌ job1, sync, task

Use Separate Definitions for Different Purposes ​

Create separate job definitions when you need distinct monitoring or different use cases:

yaml
# Same component, different purposes/monitoring
uri: /resources/job-definitions/sync-customers
spec:
  componentId: sync.hreactor

---
uri: /resources/job-definitions/sync-products
spec:
  componentId: sync.hreactor

This allows independent statistics tracking and clearer monitoring of each use case.

Check for Pending Jobs Before Deletion ​

Before deleting a job definition, verify no pending jobs exist:

http
GET /resources/jobs?jobDefinitionId=my-definition&status=pending

See Also ​

© 2026 Hantera AB. Org. no.: 559242-9582