How Probe Handles Job Dependencies with the `needs` Keyword
Probe implements job dependencies through a Needs []string field on the Job struct, validated by a JobScheduler that checks for circular dependencies and ensures a job only runs after all declared upstream jobs complete successfully.
Probe is a lightweight workflow runner inspired by GitHub Actions. When you define jobs in a workflow file, the needs keyword lets you specify which jobs must finish before another can start. This article explains how Probe parses these dependencies, validates them at startup, and enforces them during execution according to the source code in linyows/probe.
How the needs Field Declares Dependencies
The Job Struct Definition
In main/job.go, the Job struct contains a Needs field that captures the dependency list from your YAML workflow:
type Job struct {
ID string `yaml:"id,omitempty"`
Name string `yaml:"name,omitempty"`
Needs []string `yaml:"needs,omitempty"` // ← dependency names
Steps []Step `yaml:"steps,omitempty"`
// … other fields …
}
When Probe loads a workflow, it unmarshals the YAML into these structs. The needs array holds the id or name values of jobs that must complete before this job can run.
Building and Validating the Dependency Graph
Registering Jobs in the Scheduler
Before execution begins, Probe creates a JobScheduler in main/scheduler.go to manage the workflow state. The AddJob method registers each job, generating an ID if one is missing and initializing tracking maps for status, results, and repeat counters:
func (js *JobScheduler) AddJob(job Job) string {
if job.ID == "" {
job.ID = generateJobID()
}
js.jobs[job.ID] = job
js.status[job.ID] = JobPending
// … initialize repeat counters …
return job.ID
}
Validating Dependencies and Detecting Cycles
Once all jobs are registered, ValidateDependencies performs two critical checks:
- Existence validation: Every name in
Needsmust correspond to a registered job ID. - Cycle detection: The scheduler uses a DAG utility (
dag.DetectCycleFn) to ensure no circular dependencies exist (e.g.,AneedsB,BneedsA).
func (js *JobScheduler) ValidateDependencies() error {
// 1️⃣ Ensure every `need` points to an existing job
for _, job := range js.jobs {
for _, dep := range job.Needs {
if _, ok := js.jobs[dep]; !ok {
return fmt.Errorf("job '%s' depends on non-existent job '%s'", job.ID, dep)
}
}
}
// 2️⃣ Detect cycles using the DAG helper
return js.checkCircularDependencies()
}
If validation fails, Probe aborts the workflow immediately with an error message.
Runtime Execution and Dependency Resolution
Checking if a Job Can Run
During the execution phase, the scheduler determines readiness via CanRunJob. A job is runnable only when all jobs listed in its Needs array have fully completed all their repeat iterations successfully:
func (js *JobScheduler) CanRunJob(jobID string) bool {
job := js.jobs[jobID]
if js.status[jobID] != JobPending {
return false
}
// All dependencies must be fully completed (including repeats)
for _, dep := range job.Needs {
if !js.isJobFullyCompleted(dep) {
return false
}
}
return true
}
The helper isJobFullyCompleted verifies both the job status and that the success result is true:
func (js *JobScheduler) isJobFullyCompleted(jobID string) bool {
if js.status[jobID] != JobCompleted {
return false
}
target := js.repeatTargets[jobID]
counter := js.repeatCounters[jobID]
return counter >= target && js.results[jobID]
}
The Main Execution Loop
The workflow engine in main/workflow.go orchestrates execution through startJobsWithDependencies. It repeatedly queries the scheduler for runnable jobs and launches them concurrently:
func (w *Workflow) startJobsWithDependencies(ctx JobContext) error {
for !ctx.JobScheduler.AllJobsCompleted() {
runnable := ctx.JobScheduler.GetRunnableJobs()
if len(runnable) == 0 {
// No job can proceed → mark jobs whose dependencies failed as skipped
w.handleNoRunnableJobs(ctx)
continue
}
w.processRunnableJobs(runnable, ctx)
ctx.JobScheduler.wg.Wait()
}
return nil
}
GetRunnableJobs iterates over all pending jobs and returns those where CanRunJob returns true.
Handling Failed Dependencies and Skipped Jobs
When a dependency fails, Probe prevents downstream execution. The MarkJobsWithFailedDependencies method in main/scheduler.go identifies jobs whose dependencies have failed or completed unsuccessfully, marking them as failed:
func (js *JobScheduler) MarkJobsWithFailedDependencies() []string {
var skipped []string
for id, job := range js.jobs {
if js.status[id] != JobPending {
continue
}
for _, dep := range job.Needs {
if js.status[dep] == JobFailed || (js.status[dep] == JobCompleted && !js.results[dep]) {
js.status[id] = JobFailed
js.results[id] = false
skipped = append(skipped, id)
break
}
}
}
return skipped
}
Finally, main/workflow.go updates the output for these skipped jobs via updateSkippedJobsOutput, setting their status to "skipped" in the final report:
func (w *Workflow) updateSkippedJobsOutput(skippedJobs []string, rs *Result) {
for _, jobID := range skippedJobs {
if jr, ok := rs.Jobs[jobID]; ok {
jr.mutex.Lock()
jr.EndTime = jr.StartTime
jr.Status = "skipped"
jr.Success = true // treated as successful, like a skipif
jr.mutex.Unlock()
}
}
}
Key Implementation Files
| File | Purpose | Link |
|---|---|---|
main/job.go |
Definition of Job (including Needs) and per-job execution logic |
job.go |
main/scheduler.go |
Job scheduler that registers jobs, validates dependencies, and decides runnable jobs | scheduler.go |
main/workflow.go |
Orchestrates the whole workflow, repeatedly invoking the scheduler, handling dead-locks & skipped jobs | workflow.go |
main/executor.go |
Executes a single job (including repeat & async handling) once the scheduler marks it runnable | executor.go |
dag/* |
Utility for detecting dependency cycles used by the scheduler | dag package |
Summary
- Declaration: Probe uses the
Needs []stringfield inmain/job.goto capture dependency lists from YAML workflow files. - Validation: The
JobSchedulerinmain/scheduler.govalidates that all referenced jobs exist and uses a DAG utility to detect circular dependencies before execution starts. - Execution: The scheduler only marks a job as runnable when
CanRunJobconfirms all dependencies have fully completed (including all repeat iterations) successfully. - Failure handling: If a dependency fails,
MarkJobsWithFailedDependenciesmarks downstream jobs as failed, andmain/workflow.goreports them as skipped in the final output.
Frequently Asked Questions
What happens if a job lists a dependency that does not exist?
Probe validates the dependency graph before running any jobs. In main/scheduler.go, the ValidateDependencies function checks that every name in a job's Needs array corresponds to a registered job ID. If a dependency is missing, Probe returns an error immediately and aborts the workflow.
Does Probe support circular dependencies between jobs?
No. Probe explicitly prevents circular dependencies. The scheduler uses the dag.DetectCycleFn utility in main/scheduler.go to check for cycles during validation. If a cycle is detected (for example, Job A needs Job B, and Job B needs Job A), the workflow fails to start with a validation error.
How does Probe handle jobs that need to run multiple times (repeats)?
Probe respects the repeat configuration when evaluating dependencies. The isJobFullyCompleted method in main/scheduler.go checks that a dependency has not only reached JobCompleted status but also that its repeat counter meets or exceeds the target and the result is successful. A dependent job will not start until all repeats of its dependencies finish successfully.
What is the difference between a failed job and a skipped job in Probe?
A failed job is one that executed but returned an error or non-zero exit code. A skipped job is one that never ran because one of its dependencies failed. In main/scheduler.go, MarkJobsWithFailedDependencies marks dependent jobs as failed internally. Then in main/workflow.go, updateSkippedJobsOutput updates the final result to show these jobs with a "skipped" status, treating them as successful for flow-control purposes but indicating they did not execute.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →