Skip to content

Scheduling Jobs

Jobs, FastAPI releases, and CodeRepository images are created from repository workflow declarations. Put a direct .yaml or .yml child under .mainsequence/workflows/, validate it with the CodeRepositoryBranch workflow endpoint, commit it, and push the exact source commit. Django resolves the resource and image identities and creates the deployment run. The SDK does not create these objects by posting selected UIDs to collection endpoints.

Create a Job from repository source

Retrieve the current template from GET /api/v1/code-repository-branches/{uid}/workflow-template/. Declare a kind: job resource with its repository-relative execution_path, compute settings, and optional schedule. Validate the proposed repository path and content with POST /api/v1/code-repository-branches/{uid}/validate-workflow/. The backend owns the accepted version, fields, defaults, permissions, and application semantics; use the current template rather than copying an old example as a schema.

Commit the workflow file and push it with Git as usual:

git add .mainsequence/workflows/jobs.yaml
git commit -m "Update job workflow"
git push

Inspect the repository-event result and the Job or deployment history after the push. A successful Git push does not itself prove deployment success.

A workflow declaration owns its Job. On the next push of the branch head, these delete the Job:

  • removing the declaration or its file;
  • a scope that excludes the branch's Environment.

Renaming the file or the key deletes the Job and creates a new one. An invalid file deletes nothing. The repository-event result lists deleted targets under deletions.

Deploy on every push or on release tags

The platform deploys from Git pushes according to the repository's .mainsequence/workflows/*.yaml. A declaration's tag_regex decides which pushes deploy:

  • tag_regex omitted, or null: every push to the branch deploys.
  • tag_regex set to a regular expression: a tag that matches it deploys the commit it points at, whether that is the branch's latest commit or an older commit on the branch. Every-push targets ignore tags.

Automatic deployment and its tag_regex are set in the workflow file only. Use the current workflow template for where they go in a declaration. A ResourceRelease or Job update does not accept them, and the SDK models expose automatic_deployment and automatic_redeployment_policy as read-only fields.

The Main Sequence platform does not create or name tags. Versions and release tags belong to the repository: raise the version in pyproject.toml (for example with uv version --bump patch) in the commit you want to release, and let the repository's own CI create the tag. The following GitHub Actions workflow is an example of such repository code, not a platform requirement. On every push to main it runs the tests and then tags the version that pyproject.toml declares, once:

# Example: .github/workflows/release.yml in your repository
name: release
on:
  push:
    branches: [main]
concurrency:
  group: release-main
  cancel-in-progress: false
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: astral-sh/setup-uv@v6
      - run: uv sync --locked
      - run: uv run pytest
      - name: Tag the version declared in pyproject.toml
        run: |
          TAG="v$(uv version --short)"
          if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
            echo "$TAG already exists; nothing to release"
            exit 0
          fi
          git tag "$TAG" "$GITHUB_SHA"
          git push origin "refs/tags/$TAG"

With this example, a tag_regex such as ^v[0-9]+\.[0-9]+\.[0-9]+$ deploys the commits that CI tagged, and a push that does not raise the version deploys nothing.

After a dependency change, run mainsequence code-repository sync to refresh uv.lock, the local environment and requirements.txt, then commit and push those files with the change. The command makes no Git commit, tag or push. A Job runs the repository's files from /workspace with the dependencies that the committed requirements.txt lists; see Dependencies and the deployed image.

Schedules and arguments

Use an interval schedule for a regular period such as every hour, or a five-field crontab expression for calendar timing. Define the schedule in the repository workflow. scheduled_command_args is an ordered list of argv entries copied into each future scheduler-created JobRun. Updating the saved list changes only future runs; existing run snapshots stay unchanged.

Manual runs use only their own command_args and never inherit scheduled arguments. Pass each argument separately:

mainsequence code-repository jobs run <JOB_UID> \
  --arg=--start-date \
  --arg=2026-09-08T16:43:00Z
mainsequence code-repository jobs run <JOB_UID> -- --family jobs

For an existing Job, jobs update <JOB_UID> --scheduled-arg=<VALUE> replaces the saved list; --clear-scheduled-args clears it. The repository workflow may reapply its declared value on a later event.

Inspect and run an existing Job

mainsequence code-repository jobs list
mainsequence code-repository jobs run <JOB_UID>
mainsequence code-repository jobs runs list <JOB_UID>
mainsequence code-repository jobs runs logs <JOB_RUN_UID> --max-wait-seconds 900
mainsequence code-repository images list

The Job always refers to an exact persisted image. Each JobRun freezes the image UID, digest, and commit selected at run creation, even if a later repository event promotes a new image for the Job.

The Python client supports the same existing-Job operations:

from mainsequence.client import Job, JobRun

jobs = Job.filter()
job = jobs[0]
run_payload = job.run_job(command_args=["--family", "jobs"])
job_runs = JobRun.filter(job__uid=job.uid)
logs = job_runs[0].get_logs()
usage = job_runs[0].get_resource_usage()

logs is an OwnerLogPage with opaque cursor pagination. usage contains aggregate CPU, memory, and disk samples. Application code does not supply an Organization Environment UID to read the run's application logs.

Caller context in a run

A run created by a request that carried caller context starts with two environment variables, in W3C header format:

  • BAGGAGE: the request's mainsequence.<application>.<name> keys;
  • TRACEPARENT: the trace the run belongs to.

A scheduled run, or a run the platform starts on its own, has neither. Importing mainsequence restores them as the process's OpenTelemetry context, so Job code reads the context with OpenTelemetry's API:

import mainsequence  # restores the run's TRACEPARENT and BAGGAGE
from opentelemetry import baggage

person = baggage.get_baggage("mainsequence.advisor.person")
if person is None:
    ...  # no external user: do only what needs none
  • The context is restored on the thread that imports the SDK. A thread you start yourself begins without it, as with any OpenTelemetry context; run its work with contextvars.copy_context().run to keep it.
  • Calls the run makes through the SDK pass the context on, so the runs and Agent Tasks it creates inherit it. A call the run makes with its run credentials and no baggage header is recorded with the run's own context; a header, when sent, wins.
  • Log lines written through the SDK's logger carry the mainsequence. keys as baggage.
  • job.run_job() called while a FastAPI release handles a request gives the run that request's context.

Missing context means no external user: a run without BAGGAGE fails closed for anything that needs one.

Who can use a Job

A Job's access is decided by its policies:

  • viewer: see the Job, its schedule, runs, logs and outputs, including the Artifacts its runs published;
  • runner: also run the Job and stop its runs;
  • editor: also change it (name, description, resources, schedule, arguments, image), post a run's status and outputs by hand, delete it, manage what its workload can reach, and see who has access;
  • owner: also decide who has access.

Each run uses compute and credits and acts with the Job's own workload access, which can be broader than yours, and run logs and outputs can contain the data the Job processed. job.run_job() and jobs run need runner or more, and jobs update needs editor or owner. The Job's creator holds owner; repository workflows create Jobs as the Organization's automation identity, which holds no policy on them. Organization admins hold every action.

Whoever may run a Job may also set the caller context of the runs they start: a run carries the context of the request that created it, whatever that context says. A Job whose code makes rules from BAGGAGE gives the policies that run it, runner, editor and owner, including the ones it takes from its branch, only to service users it trusts to vouch for that context.

A standalone Job also takes the policies of its branch: reader on the branch gives viewer, and editor or owner gives editor, whether the branch policy is held on the branch or passed down from its repository or a Project. A release's backing Job takes its release's policies instead: viewer gives viewer, editor and owner give editor, and user gives nothing. get_access() shows each of those as a computed row with source inherited. A backing Job is changed, run and deleted with its release, so on it an update raises BadRequestError and a run or a delete ConflictError, whatever you hold. Every policy on a Job gives viewer on the Artifacts its runs published; see Who can use an Artifact.

Read and change the policies with the access methods on Job:

from mainsequence.client import Job

job = Job.filter(name__contains="nightly-prices")[0]
job.attach_policy("runner", principal_type="team", principal_uid=team_uid)

answer = job.explain_access("jobs.run")
print(answer.allowed, answer.blocked_by_rule, answer.granting_policies)

The access methods address the Job by its UID alone and take no Environment; with a workload's runtime credentials they reach only the Jobs of that workload's Environment. A refused change raises PermissionDeniedError, whose payload names the required_action and the granting_policies. A repository workflow file's access entry for a Job gives its workload the policy it names, such as policy: runner. mainsequence access policies --type jobs lists the policies with their actions, and Who Has Access to an Object describes every method and model.

From the command line:

mainsequence code-repository jobs access list <JOB_UID>
mainsequence code-repository jobs access grant <JOB_UID> runner --user <USER_UID>
mainsequence code-repository jobs access revoke <JOB_UID> <BINDING_UID>
mainsequence code-repository jobs access explain <JOB_UID> jobs.run

See Jobs in the CLI reference.

Operational checks

Use repository-relative, forward-slash execution paths such as scripts/simulated_prices_launcher.py. Validate the workflow before pushing. If a scheduled run does not appear, inspect the applied Job, schedule, and repository-event result. If a run starts but fails, inspect its frozen image and logs. The CLI log command can wait longer with --max-wait-seconds.