Nenjo Docs
Pipelines

Cron and scheduling

Schedule recurring pipeline executions with cron expressions and interval-based timers.

Nenjo supports two forms of scheduled execution: cron steps within a pipeline (polling loops) and cron-triggered pipelines (recurring whole-pipeline runs via the CronManager).

Cron steps

A cron step is a pipeline step that repeatedly executes a function (agent or lambda) at a fixed interval until it signals completion or times out. This is useful for polling external systems -- for example, waiting for a PR to be merged or a deployment to finish.

Configuration

{
  "step_type": "cron",
  "role_id": "ROLE_UUID",
  "config": {
    "interval": "30s",
    "timeout": "2h"
  }
}
FieldTypeDefaultDescription
intervalstring"60s"How often to run the function
timeoutstring"24h"Maximum total time before the step aborts
role_idUUID--Agent role to run (agent mode)
lambda_idUUID--Lambda script to run (lambda mode)

The role_id and lambda_id can be set either as top-level step fields or inside the config JSON. When both are present, lambda takes precedence.

Execution modes

Agent mode -- An LLM agent runs on each polling cycle using the specified role. The agent should include a cron_status JSON signal in its output to indicate completion.

Lambda mode -- A script runs on each polling cycle. The script communicates its status via stdout.

Signaling

On each cycle, the function's output is parsed for a status signal:

{"cron_status": "pass", "reason": "PR #42 was merged successfully"}
StatusEffect
passStop the loop, take the on_pass edge
failStop the loop, take the on_fail edge
waitSleep for interval and run again
abortAbort the pipeline immediately with an error

If no signal is found in the output, the step behaves as wait and runs again on the next cycle.

Legacy format: For backward compatibility, CRON_DONE:PASS and CRON_DONE:FAIL markers in the output text are also recognized.

Example: wait for PR merge

{
  "name": "Wait for PR Merge",
  "step_type": "cron",
  "lambda_id": "LAMBDA_UUID",
  "config": {
    "interval": "5m",
    "timeout": "12h"
  }
}

The lambda script checks the PR status and outputs:

#!/bin/bash
PR_STATE=$(gh pr view "$NENJO_PR_NUMBER" --json state -q '.state')
if [ "$PR_STATE" = "MERGED" ]; then
  echo '{"cron_status": "pass", "reason": "PR merged"}'
elif [ "$PR_STATE" = "CLOSED" ]; then
  echo '{"cron_status": "fail", "reason": "PR was closed without merging"}'
else
  echo '{"cron_status": "wait"}'
fi

Cron-triggered pipelines

Separately from cron steps, entire pipelines can be triggered on a recurring schedule. This is managed by the CronManager on the worker.

Pipeline configuration

Set the pipeline's trigger to cron and add a schedule to the metadata:

curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Daily Code Audit",
    "trigger": "cron",
    "metadata": {
      "schedule": "0 9 * * *"
    }
  }' \
  https://your-instance.com/api/v1/pipelines

Schedule format

The schedule field supports two formats:

Cron expressions -- Standard 5-field cron syntax (minute, hour, day-of-month, month, day-of-week):

ExpressionMeaning
0 9 * * *Every day at 9:00 AM
*/30 * * * *Every 30 minutes
0 0 * * 1Every Monday at midnight

Interval strings -- Simple duration strings for fixed-interval scheduling:

IntervalMeaning
30sEvery 30 seconds
5mEvery 5 minutes
1hEvery hour
2dEvery 2 days

The format is auto-detected: if the string contains spaces, it is treated as a cron expression; otherwise, as an interval.

CronManager

The CronManager runs on the worker and manages the lifecycle of all cron-triggered pipeline schedules.

Bootstrap

On worker startup, the CronManager:

  1. Reads all active cron assignments (pipeline-to-project mappings with trigger type cron) and starts a timer for each.
  2. Reads all active cron pipelines that have a schedule in their metadata but no active assignment, and starts standalone timers for those.

Operations

OperationEventDescription
Enablecron.enableStart a new schedule timer. Cancels any existing timer for the same assignment.
Disablecron.disableCancel the timer for an assignment.
Trigger nowcron.triggerFire a one-off execution immediately without affecting the regular schedule.

Router events

The backend pushes events to the worker via WebSocket when schedule state changes:

{"type": "cron.enable", "assignment_id": "...", "pipeline_id": "...", "project_id": "...", "schedule": "0 9 * * *"}
{"type": "cron.disable", "assignment_id": "..."}
{"type": "cron.trigger", "pipeline_id": "...", "project_id": "..."}

Manual trigger

You can manually trigger a cron pipeline via the API:

curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-instance.com/api/v1/pipelines/$PIPELINE_ID/trigger

This returns 202 Accepted and pushes a cron.trigger event to the worker. Only pipelines with trigger: "cron" can be manually triggered.

Cron execution flow

When a cron timer fires:

  1. An execution run record is created with status: "running".
  2. The pipeline is loaded from cached bootstrap data.
  3. The pipeline executes with is_cron_trigger: true (agent steps use cron_task templates).
  4. On completion, a ticket_completed WebSocket message is sent so the backend can update the execution status.

Heartbeat and status

The CronManager tracks:

FieldDescription
assignment_idThe assignment or pipeline ID this schedule belongs to
pipeline_idThe pipeline being scheduled
last_run_atTimestamp of the last execution (ISO 8601)
next_fire_atComputed next fire time (ISO 8601)

Cron steps vs. cron pipelines

FeatureCron stepCron pipeline
ScopeSingle step within a pipelineEntire pipeline
TriggerRuns as part of pipeline executionRuns on its own schedule
Use casePolling/waiting within a workflowRecurring autonomous tasks
SignalFunction signals pass/fail/wait/abortPipeline completes normally
ManagerExecutor loopCronManager on worker

On this page