Skip to content

Finding codes

Every finding the Rhylthyme validator reports has a code, a message, a where (the step, track or path it is about) and a fix. This page lists every code: what it means, a program that triggers it, and how to fix it. The codes are those of validateProgram in @rhylthyme/timeline/schedule.

The hosted MCP server

The MCP tool validate_program and the checks visualize_schedule runs before it publishes still use the server's own copy of the validator. It reports many of the codes below, and also these, which this page does not list: missing_program_id, missing_name, no_tracks, bad_track, missing_track_id, missing_track_name, bad_step, missing_step_id, missing_step_name, missing_start_trigger, bad_duration_type, fixed_without_seconds, variable_without_bounds, indefinite_without_default, choice_too_few_options and bad_alert. validateProgram reports each of those problems as a schema_* finding instead (they were removed from it in 2.0.0-beta.7).

Errors make a program invalid; warnings and info notes do not. The Python column says what the rhylthyme package's validator reports for the same program: rhylthyme validate --json (the CLI, which checks the program as written) and validate_program_structured (the library, which expands replicates first). It reports schema problems as one schema_error and has no code of its own for many logic checks.

This page is generated from the catalogue that @rhylthyme/timeline ships (findingCodes, version 2.0.0-beta.7 here) by tools/gen-finding-codes.js; a test fails when it falls behind.

All codes

Base program

Each example below is a fragment: its top-level keys replace those of this valid program, which has no findings at all.

{
  "programId": "example",
  "name": "Example",
  "schemaVersion": "0.2.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 600 },
          "startTrigger": { "type": "afterStep", "stepId": "prep" }
        }
      ]
    }
  ],
  "resourceConstraints": []
}

Schema

The program does not match the JSON Schema its schemaVersion selects (rhylthyme-spec's, bundled), or cannot be checked against it.

Code Severity Summary Python
not_an_object error The program is not a JSON object. no
not_json error The program cannot be written as JSON, so the schema was not checked. no
schema_type error A value has the wrong JSON type. schema_error
schema_enum error A value is not one of the allowed choices. schema_error
schema_const error A value is not the one fixed value the schema allows. (not reachable today) schema_error
schema_required error A required property is missing. schema_error
schema_additionalProperties error An object has a property the schema does not allow. schema_error
schema_minItems error A list has too few items. schema_error
schema_maxItems error A list has too many items. (not reachable today) schema_error
schema_minLength error A string is shorter than allowed (usually empty). schema_error
schema_maxLength error A string is longer than allowed. schema_error
schema_pattern error A string does not match the required pattern. schema_error
schema_minimum error A number is below the minimum. schema_error
schema_maximum error A number is above the maximum. schema_error
schema_exclusiveMinimum error A number must be greater than a bound and is not. schema_error
schema_exclusiveMaximum error A number must be less than a bound and is not. (not reachable today) schema_error
schema_oneOf error A value matches none, or more than one, of the allowed shapes. schema_error
schema_anyOf error A value matches none of the alternatives it may take. schema_error
schema_not error A value has a combination of keys the schema forbids. schema_error
schema_false error A value is present where the schema allows nothing. (not reachable today) schema_error

not_an_object

Error. The program is not a JSON object.

validateProgram was given an array, a string, a number, null or another non-object. Nothing else is checked: the result has this one error and stats null.

Example. Passed to validateProgram as it is:

["not", "a", "program"]

The whole value passed to validateProgram, not a fragment.

Fix. Pass the full program object ({"programId": ..., "tracks": [...]}), not a string, an array or a wrapper around it.

Python. Python reports nothing for it. Python raises on a program that is not an object instead of reporting a finding.

not_json

Error. The program cannot be written as JSON, so the schema was not checked.

A JavaScript caller passed a value JSON cannot carry: a circular reference, a BigInt or similar. The JSON Schema check is skipped and this finding is reported in its place; the logic checks still run. Programs read from JSON text never get it.

Example. Merged over the base program:

{ "metadata": { "bytes": 10 } }

metadata.bytes is passed as a JavaScript BigInt, which no JSON text can carry.

Fix. Pass plain JSON data: no circular references, BigInt values, functions or class instances.

Python. Python reports nothing for it. Python always validates parsed JSON.

schema_type

Error. A value has the wrong JSON type.

The schema expects a different type at this path: a string for a name, an integer for maxConcurrent, an object for a duration. Numbers written in quotes ("2") are strings. A value of the wrong type is reported once, not again by the oneOf/anyOf beside it.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": 7,
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Write the value as the type the message names; for a quoted number, drop the quotes.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_enum

Error. A value is not one of the allowed choices.

The field accepts a fixed set of strings (an alert's level, a replicates mode, a trigger's event). Choices that tell apart the shapes of a oneOf, such as a duration's type, are reported as schema_oneOf instead.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "alerts": [{ "event": "end", "level": "loud" }]
        }
      ]
    }
  ]
}

Fix. Use one of the values listed in the message.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_const

Error. A value is not the one fixed value the schema allows.

A const keyword pins a field to one value.

Not reachable today

The bundled 0.3.0-alpha schema uses const only inside an if (metadata.varianceFactors), where a mismatch selects the other branch instead of failing, and 0.2.0-alpha does not use it. The checker supports it so that a schema update that starts using it keeps working; test/finding-codes.test.js fails when that happens, asking for an example here.

Fix. Set the field to the value the message names.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_required

Error. A required property is missing.

Every program needs programId, name, tracks and resourceConstraints (or environment / environmentType); every track a trackId, name and steps; every step a stepId, name and startTrigger; every resource constraint a task and maxConcurrent. A missing startTrigger's fix names the step to chain after.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Add the property the message names.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_additionalProperties

Error. An object has a property the schema does not allow.

Some objects are closed (instrument, instrument actions, alerts, ...): any key besides the listed ones is an error, usually a typo. Programs, tracks, steps and triggers are open and accept extra keys.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "toolType": "bioshake",
            "command": "start_shake",
            "params": { "speed": 1000, "duration": 300 },
            "retries": 2
          }
        }
      ]
    }
  ]
}

Fix. Remove the key or fix its spelling; the fix lists the keys allowed there.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_minItems

Error. A list has too few items.

Lists such as a compound trigger's triggers (at least 2), an instrument's start/end actions and a step's alerts must not be empty or too short.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "alerts": []
        }
      ]
    }
  ]
}

Fix. Add items, or remove the optional list entirely.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_maxItems

Error. A list has too many items.

A maxItems keyword caps a list's length.

Not reachable today

No bundled program schema (0.2.0-alpha, 0.3.0-alpha) uses maxItems where a failure is reported, so validateProgram cannot emit this code today. The checker supports it so that a schema update that starts using it keeps working; test/finding-codes.test.js fails when that happens, asking for an example here.

Fix. Shorten the list.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_minLength

Error. A string is shorter than allowed (usually empty).

Names that must not be empty: an instrument's tool and command, an alert's message.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "instrument": { "tool": "", "command": "start_shake" }
        }
      ]
    }
  ]
}

Fix. Give the string a value, or remove the optional field.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_maxLength

Error. A string is longer than allowed.

An alert's message is at most 200 characters (code points, as Python counts them).

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "alerts": [
            {
              "event": "end",
              "message": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
            }
          ]
        }
      ]
    }
  ]
}

Fix. Shorten the string to the length the message names.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_pattern

Error. A string does not match the required pattern.

Schema 0.3.0-alpha constrains a few identifiers: metadata.varianceFactors keys (a letter, then letters, digits, _ and -) and a duration's calibratedFrom.programVersion ("sha256:" and 64 hex digits; inside a duration it is reported as schema_oneOf).

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "metadata": { "varianceFactors": [{ "key": "2nd oven", "type": "number" }] }
}

Fix. Make the string match the pattern in the message.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_minimum

Error. A number is below the minimum.

maxConcurrent is at least 1; durations and their bounds are at least 0.

Example. Merged over the base program:

{ "resourceConstraints": [{ "task": "oven", "maxConcurrent": 0 }] }

Fix. Use a value at or above the minimum the message names.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_maximum

Error. A number is above the maximum.

A taskResources fraction (the share of a resource a step occupies) is at most 1.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "taskResources": [{ "name": "oven", "fraction": 1.5 }]
        }
      ]
    }
  ]
}

Also emits task_not_constrained for the oven.

Fix. Use a value at or below the maximum the message names.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_exclusiveMinimum

Error. A number must be greater than a bound and is not.

An instrument's or action's timeoutSeconds must be greater than 0.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "toolType": "bioshake",
            "command": "start_shake",
            "params": { "speed": 1000, "duration": 300 },
            "timeoutSeconds": 0
          }
        }
      ]
    }
  ]
}

Fix. Use a value greater than the bound, or remove the optional field.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_exclusiveMaximum

Error. A number must be less than a bound and is not.

An exclusiveMaximum keyword caps a number below a bound.

Not reachable today

No bundled program schema (0.2.0-alpha, 0.3.0-alpha) uses exclusiveMaximum where a failure is reported, so validateProgram cannot emit this code today. The checker supports it so that a schema update that starts using it keeps working; test/finding-codes.test.js fails when that happens, asking for an example here.

Fix. Use a value below the bound.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_oneOf

Error. A value matches none, or more than one, of the allowed shapes.

Durations, start triggers and the program's own startTrigger have several shapes told apart by their type. A value that fits none gets the closest shape's first problem (or the list of allowed types); a value that fits two (a duration with both seconds and timeString) is ambiguous.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixd", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Follow the fix: set type to an allowed value, add the keys the closest shape needs, or remove the keys that make it ambiguous.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_anyOf

Error. A value matches none of the alternatives it may take.

A program needs resourceConstraints, environment or environmentType; an instrument needs a command, an until, or start/end actions. The message lists the alternatives.

Example. Merged over the base program, without resourceConstraints:

{}

Fix. Add one of the alternatives the message lists.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_not

Error. A value has a combination of keys the schema forbids.

An instrument has both command and until (only one call can end the step); a program has resourceConstraints together with environment or environmentType.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "command": "start_shake",
            "until": { "command": "wait" }
          }
        }
      ]
    }
  ]
}

Also emits instrument_bad_shape.

Fix. Keep only one of the keys the message names.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

schema_false

Error. A value is present where the schema allows nothing.

A false subschema forbids any value at that path.

Not reachable today

No bundled program schema (0.2.0-alpha, 0.3.0-alpha) uses a false subschema (a boolean schema) where a failure is reported, so validateProgram cannot emit this code today. The checker supports it so that a schema update that starts using it keeps working; test/finding-codes.test.js fails when that happens, asking for an example here.

Fix. Remove the value.

Python. Python reports schema_error. Python reports the first schema violation only, as one schema_error whose message is jsonschema's.

Structure and references

Ids, triggers and the references between steps.

Code Severity Summary Python
program_id_format warning programId is not kebab-case. no
missing_schema_version warning schemaVersion is missing. no
empty_track warning A track has no steps; it is left out. schema_error (CLI), none (library)
duplicate_track_id error Two tracks share a trackId. no
duplicate_step_id error A stepId is used more than once. duplicate_step_id
compound_without_triggers error A trigger has logic but no triggers list. no
bad_trigger_logic error A trigger's logic is not "all" or "any". no
bad_trigger error A trigger inside a compound trigger is not an object. (not reachable today) no
unknown_trigger_type error A trigger's type is not one the runner knows. no
trigger_without_step_id error An afterStep, afterStepWithBuffer or onAbort trigger has an empty stepId. dangling_step_ref
dangling_step_ref error A trigger waits for a step that does not exist. dangling_step_ref
self_reference error A step waits for itself. no
buffer_without_seconds error An afterStepWithBuffer trigger has no bufferSeconds. no
bad_trigger_event error A trigger's event is not "start" or "end". no
offset_without_seconds error A programStartOffset trigger has no offsetSeconds. no
choice_ref_no_choice error A trigger's choiceId refers to a step that offers no choice. choice_ref
choice_ref_unknown error A trigger's choiceId is not one of the referenced step's options. choice_ref

program_id_format

Warning. programId is not kebab-case.

programId should start with a letter or digit and contain only letters, digits, dashes and underscores, so it is safe in URLs and file names.

Example. Merged over the base program:

{ "programId": "My Program!" }

Fix. Use lowercase letters, digits and dashes, e.g. "my-program".

Python. Python reports nothing for it.

missing_schema_version

Warning. schemaVersion is missing.

Without schemaVersion the program is checked against the 0.2.0-alpha schema and the viewer assumes 0.1.0. instances triggers and maxInFlight need 0.3.0-alpha.

Example. Merged over the base program, without schemaVersion:

{}

Fix. Set schemaVersion: "0.1.0", "0.2.0-alpha" (choice branching) or "0.3.0-alpha" (instances on triggers).

Python. Python reports nothing for it.

empty_track

Warning. A track has no steps; it is left out.

A track whose steps list is missing or empty (and that names no templateId) is dropped before the other checks, as Python's expander drops it.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    { "trackId": "idle", "name": "Idle", "steps": [] }
  ]
}

Fix. Add steps to the track or remove it.

Python. rhylthyme validate reports schema_error, as an error; validate_program_structured reports nothing. rhylthyme validate checks the program as written, where an empty steps list breaks the schema; validate_program_structured drops the track while expanding, as JS does, and says nothing.

duplicate_track_id

Error. Two tracks share a trackId.

trackIds must be unique: the renderer, the runner and replicate expansion address tracks by id.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Give every track its own trackId.

Python. Python reports nothing for it.

duplicate_step_id

Error. A stepId is used more than once.

stepIds must be unique across the whole program, not only within a track, since triggers refer to steps by id alone. Replicate copies get ids "-r"; a hand-written step with such an id collides with them.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Rename one of the steps and update the triggers that point at it.

Python. Python reports duplicate_step_id.

compound_without_triggers

Error. A trigger has logic but no triggers list.

logic ("all"/"any") combines the triggers in triggers; on a single trigger it has nothing to combine. The schema accepts the extra key on a single trigger, so this check names it.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart", "logic": "all" }
        }
      ]
    }
  ]
}

Fix. Remove logic, or make the trigger compound: {"logic": "all", "triggers": [...]} with at least two triggers.

Python. Python reports nothing for it.

bad_trigger_logic

Error. A trigger's logic is not "all" or "any".

Reported when the trigger otherwise passes the schema as a single trigger that also carries logic and triggers keys; a compound trigger with a bad logic value is a schema_oneOf finding.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "type": "programStart",
            "logic": "xor",
            "triggers": [
              { "type": "afterStep", "stepId": "prep" },
              { "type": "programStart" }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Use logic "all" (every trigger must fire) or "any" (the first one starts the step).

Python. Python reports nothing for it.

bad_trigger

Error. A trigger inside a compound trigger is not an object.

Meant for a non-object entry in a compound trigger's triggers list.

Not reachable today

Non-object entries are dropped when the trigger is flattened, before this check looks at them, and the schema reports them (schema_type / schema_oneOf) wherever they would matter. Dead code in validateProgram.

Fix. Make every entry of triggers an object such as {"type": "afterStep", "stepId": "..."}.

Python. Python reports nothing for it.

unknown_trigger_type

Error. A trigger's type is not one the runner knows.

Known types: programStart, programStartOffset, afterStep, afterStepWithBuffer, manual, onAbort. A bad type on a trigger the schema checks is a schema_oneOf finding; this check reaches the entries of a triggers list on a trigger the schema took as a single trigger.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "type": "programStart",
            "logic": "all",
            "triggers": [
              { "type": "afterStepp", "stepId": "prep" },
              { "type": "programStart" }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Use programStart, programStartOffset, afterStep, afterStepWithBuffer, manual or onAbort.

Python. Python reports nothing for it.

trigger_without_step_id

Error. An afterStep, afterStepWithBuffer or onAbort trigger has an empty stepId.

These triggers wait for another step and need its id. The schema requires the key; this check catches an empty string.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "" }
        }
      ]
    }
  ]
}

Fix. Set stepId to the step this one waits for.

Python. Python reports dangling_step_ref. Python reports the empty id as a reference to a step that does not exist.

dangling_step_ref

Error. A trigger waits for a step that does not exist.

The stepId in an afterStep, afterStepWithBuffer or onAbort trigger matches no step. Ids are case-sensitive; after replicate expansion a replicated step's copies are "-r1", "-r2", ... and the authored id is kept for barriers.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "Prep" }
        }
      ]
    }
  ]
}

Fix. Point the trigger at an existing stepId (check case and spelling).

Python. Python reports dangling_step_ref.

self_reference

Error. A step waits for itself.

A step's trigger names its own stepId, so it can never start.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "prep" }
        }
      ]
    }
  ]
}

Also emits dependency_cycle.

Fix. Point the trigger at the step before it, or use programStart.

Python. Python reports nothing for it. Python reports nothing; the step is left unscheduled.

buffer_without_seconds

Error. An afterStepWithBuffer trigger has no bufferSeconds.

The schema requires bufferSeconds on a trigger it checks; this check reaches the entries of a triggers list on a trigger the schema took as a single trigger.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "type": "programStart",
            "logic": "all",
            "triggers": [
              { "type": "afterStepWithBuffer", "stepId": "prep" },
              { "type": "programStart" }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Add bufferSeconds (seconds or a time string such as "5m"), or use afterStep.

Python. Python reports nothing for it.

bad_trigger_event

Error. A trigger's event is not "start" or "end".

event chooses whether the trigger anchors on the referenced step's start or end (the default). A bad value on a trigger the schema checks is a schema finding; this check reaches the entries of a triggers list on a trigger the schema took as a single trigger.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "type": "programStart",
            "logic": "all",
            "triggers": [
              { "type": "afterStep", "stepId": "prep", "event": "middle" },
              { "type": "programStart" }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Use event "start" or "end", or leave it out ("end").

Python. Python reports nothing for it.

offset_without_seconds

Error. A programStartOffset trigger has no offsetSeconds.

The schema requires offsetSeconds on a trigger it checks; this check reaches the entries of a triggers list on a trigger the schema took as a single trigger.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "type": "programStart",
            "logic": "all",
            "triggers": [{ "type": "programStartOffset" }, { "type": "programStart" }]
          }
        }
      ]
    }
  ]
}

Fix. Add offsetSeconds (seconds or a time string such as "10m"), or use programStart.

Python. Python reports nothing for it.

choice_ref_no_choice

Error. A trigger's choiceId refers to a step that offers no choice.

choiceId makes a step run only when the referenced step's choice selects that option (schema 0.2.0-alpha branching). The referenced step has no choice.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "prep", "choiceId": "red" }
        }
      ]
    }
  ]
}

Fix. Add a choice with that option to the referenced step, or remove choiceId.

Python. Python reports choice_ref. Python has one code for both choiceId problems.

choice_ref_unknown

Error. A trigger's choiceId is not one of the referenced step's options.

The referenced step offers a choice, but none of its options has this choiceId. The message lists the options.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "pick",
          "name": "Pick",
          "duration": { "type": "fixed", "seconds": 60 },
          "startTrigger": { "type": "programStart" },
          "choice": {
            "prompt": "Sauce?",
            "options": [
              { "choiceId": "red", "label": "Red" },
              { "choiceId": "white", "label": "White" }
            ]
          }
        },
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "pick", "choiceId": "pesto" }
        }
      ]
    }
  ]
}

Fix. Use one of the option ids the message lists (ids are case-sensitive).

Python. Python reports choice_ref. Python has one code for both choiceId problems.

Timing and overlap

Durations, the resolved timeline, cycles and steps that overlap in a track.

Code Severity Summary Python
missing_duration error A step has no duration. no
unparseable_duration error A fixed duration's seconds is not a positive number or time string. no
variable_min_gt_max error A variable duration's minSeconds is greater than its maxSeconds. no
variable_without_default warning A variable duration has no defaultSeconds; planning uses maxSeconds. no
negative_offset_on_fixed warning A negative offset counts down into a step that is not indefinite. negative_offset
dependency_cycle error Steps wait for each other in a cycle. no
track_overlap error Two steps in one track overlap in time. track_overlap
tracks_finish_far_apart warning Some tracks finish long before the last one. no

missing_duration

Error. A step has no duration.

Every step needs a duration, except an instrument step whose command or until call ends it (see I_INSTRUMENT_DURATION) or one with only start/end actions (see instrument_no_end).

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        { "stepId": "prep", "name": "Prep", "startTrigger": { "type": "programStart" } }
      ]
    }
  ]
}

Fix. Add duration: {"type": "fixed", "seconds": 300} (or "5m"), variable with min/max, or indefinite.

Python. Python reports nothing for it. Python times a step with no duration as 0 s.

unparseable_duration

Error. A fixed duration's seconds is not a positive number or time string.

seconds may be a number or a string such as "90s", "5m", "1h30m". A string that does not parse ("soon") or a negative one is reported here; the schema accepts any string.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": "soon" },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Use a number of seconds or a time string like "5m" or "1h30m".

Python. Python reports nothing for it.

variable_min_gt_max

Error. A variable duration's minSeconds is greater than its maxSeconds.

A variable step can end anywhere between minSeconds and maxSeconds; the bounds are the wrong way round.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": {
            "type": "variable",
            "minSeconds": 600,
            "maxSeconds": 300,
            "defaultSeconds": 450
          },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Swap the bounds so minSeconds <= maxSeconds.

Python. Python reports nothing for it.

variable_without_default

Warning. A variable duration has no defaultSeconds; planning uses maxSeconds.

Previews, the makespan and the analyzer place a variable step at defaultSeconds; without it they use maxSeconds, the worst case.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "variable", "minSeconds": 300, "maxSeconds": 600 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Add defaultSeconds (the expected time).

Python. Python reports nothing for it.

negative_offset_on_fixed

Warning. A negative offset counts down into a step that is not indefinite.

A negative offsetSeconds on afterStep starts this step before the referenced step ends. The live runner can only honour that when the referenced step is indefinite (its end is announced, so the countdown has an anchor); against a fixed or variable step it is ignored at run time.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 600 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "type": "afterStep",
            "stepId": "prep",
            "offsetSeconds": -120
          }
        }
      ]
    }
  ]
}

The two steps overlap, so track_overlap is reported too; put them in separate tracks to see the warning alone.

Fix. Give the referenced step duration.type "indefinite", or use a positive offset from event "start".

Python. Python reports negative_offset, as an error. An error in Python, which also rejects a negative offset on any trigger other than afterStep / afterStepWithBuffer.

dependency_cycle

Error. Steps wait for each other in a cycle.

Each step in the cycle waits (directly or transitively) for itself, so none of them can be scheduled. Reported once per step left unscheduled whose references all exist.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "cook" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "prep" }
        }
      ]
    }
  ]
}

Fix. Break the cycle: start one of the steps from programStart or from a step outside the cycle.

Python. Python reports nothing for it. Python reports nothing; the steps are left unscheduled.

track_overlap

Error. Two steps in one track overlap in time.

Steps in a track run one after another (a track is one person, machine or lane). Two steps whose resolved times overlap usually lack an afterStep chain. Steps on different choice branches are exempt.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Chain the later step with {"type": "afterStep", "stepId": ""}, or move it to its own track if it really runs in parallel.

Python. Python reports track_overlap.

tracks_finish_far_apart

Warning. Some tracks finish long before the last one.

At least one track ends more than 30 minutes (or half the makespan, whichever is more) before the program does. Often fine; but if everything should be ready together, the early tracks start too soon.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 7200 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    },
    {
      "trackId": "side",
      "name": "Side",
      "steps": [
        {
          "stepId": "cook",
          "name": "Cook",
          "duration": { "type": "fixed", "seconds": 600 },
          "startTrigger": { "type": "programStart" }
        }
      ]
    }
  ]
}

Fix. Delay the early tracks with programStartOffset or afterStep so they converge at the end, or ignore it when finishing apart is intended.

Python. Python reports nothing for it.

Resources

Tasks and resourceConstraints.

Code Severity Summary Python
bad_resource_constraint error A resource constraint has an empty task name. no
duplicate_constraint warning Two resource constraints name the same task. no
bad_max_concurrent error A resource constraint's maxConcurrent is not an integer >= 1. no
task_not_constrained error, warning A task used by steps has no resource constraint. task_not_constrained (with --strict) only
unused_constraint warning A resource constraint names a task no step uses. no

bad_resource_constraint

Error. A resource constraint has an empty task name.

Each entry of resourceConstraints limits one task type. A missing task is a schema_required finding; an empty string is reported here.

Example. Merged over the base program:

{ "resourceConstraints": [{ "task": "", "maxConcurrent": 1 }] }

Fix. Set task to the task name the steps use, or remove the entry.

Python. Python reports nothing for it.

duplicate_constraint

Warning. Two resource constraints name the same task.

Only one limit can apply to a task; which one wins depends on the reader.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "task": "oven"
        }
      ]
    }
  ],
  "resourceConstraints": [
    { "task": "oven", "maxConcurrent": 1 },
    { "task": "oven", "maxConcurrent": 2 }
  ]
}

Fix. Keep one constraint per task.

Python. Python reports nothing for it.

bad_max_concurrent

Error. A resource constraint's maxConcurrent is not an integer >= 1.

The schema reports a bad maxConcurrent itself (schema_minimum, schema_type, schema_required), and this check stays quiet where it has. A program with an instances or replicates error (an E_* code) is checked against the schema as written, and those schema findings do not quiet the checks that follow, so this code is reported next to the schema's.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "mix",
          "name": "Mix",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "cool",
          "name": "Cool",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "mix", "instances": "each" }
        }
      ]
    }
  ],
  "resourceConstraints": [{ "task": "oven", "maxConcurrent": 0 }]
}

Also emits E_INSTANCES_ON_SINGLE, schema_minimum and unused_constraint.

Fix. Set maxConcurrent to a whole number of at least 1.

Python. Python reports nothing for it.

task_not_constrained

Error or Warning. A task used by steps has no resource constraint.

Every task named by a step (task, tasks, taskResources, buffer taskResources) needs an entry in resourceConstraints, so the runner knows how many can run at once. It is an error, unless the program names an environment or actors, which may supply the constraint: then it is a warning.

Example 1 (error). Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "task": "oven"
        }
      ]
    }
  ]
}

Example 2 (warning). Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "task": "oven"
        }
      ]
    }
  ],
  "actors": 2
}

Fix. Add {"task": "", "maxConcurrent": N} to resourceConstraints.

Python. Python reports nothing for it by default. rhylthyme validate --strict reports task_not_constrained, as an error. That includes a program that names an environment or actors, where validateProgram reports a warning. The library (validate_program_structured) has no strict mode.

unused_constraint

Warning. A resource constraint names a task no step uses.

Usually a typo in the task name on the constraint or on the steps.

Example. Merged over the base program:

{ "resourceConstraints": [{ "task": "oven", "maxConcurrent": 1 }] }

Fix. Remove the constraint, or set task on the steps that use it.

Python. Python reports nothing for it.

Instances and replicates

Schema 0.3.0-alpha instances triggers, replicates and maxInFlight, and replicate expansion.

Code Severity Summary Python
E_INSTANCES_ON_SINGLE error instances is used on a reference to a step that is not replicated. E_INSTANCES_ON_SINGLE
E_EACH_WITH_REPLICATES error A step has both an instances: "each" trigger and its own replicates. E_EACH_WITH_REPLICATES
E_EACH_COUNT_MISMATCH error A compound "each" trigger pairs groups of different sizes. E_EACH_COUNT_MISMATCH
E_INFLIGHT_GT_COUNT error replicates.maxInFlight is greater than replicates.count. E_INFLIGHT_GT_COUNT
E_INFLIGHT_NO_CHAIN error maxInFlight on a serial replicate with no "each" descendants has no effect. E_INFLIGHT_NO_CHAIN
W_UNBARRIERED_CHAIN warning An instances: "each" chain has no "all" barrier, but later steps do not wait for it. W_UNBARRIERED_CHAIN
I_IMPLICIT_BARRIER info A 0.3.0 program waits for a replicated step without saying instances. no
expansion_failed error Replicates could not be expanded. none (CLI), expansion_failed (library)

E_INSTANCES_ON_SINGLE

Error. instances is used on a reference to a step that is not replicated.

instances ("each", "all", "any") says how a reference fans in over a replicated step's copies. The referenced step has no replicates and is not itself per-instance, so there is nothing to fan in.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "mix",
          "name": "Mix",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "cool",
          "name": "Cool",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "mix", "instances": "each" }
        }
      ]
    }
  ]
}

Fix. Remove instances, or add replicates to the referenced step.

Python. Python reports E_INSTANCES_ON_SINGLE.

E_EACH_WITH_REPLICATES

Error. A step has both an instances: "each" trigger and its own replicates.

instances: "each" already makes one copy of the step per upstream instance; its own replicates would multiply them again.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "mix",
          "name": "Mix",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "bake",
          "name": "Bake",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "mix" },
          "replicates": { "count": 3 }
        },
        {
          "stepId": "cool",
          "name": "Cool",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "bake", "instances": "each" },
          "replicates": { "count": 2 }
        }
      ]
    }
  ]
}

Fix. Drop replicates on the step; it inherits the upstream count.

Python. Python reports E_EACH_WITH_REPLICATES.

E_EACH_COUNT_MISMATCH

Error. A compound "each" trigger pairs groups of different sizes.

Instance i of the step pairs with instance i of every "each" upstream, so all of them need the same count.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "sauce",
      "name": "Sauce",
      "steps": [
        {
          "stepId": "sauce",
          "name": "Sauce",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "replicates": { "count": 2 }
        }
      ]
    },
    {
      "trackId": "pasta",
      "name": "Pasta",
      "steps": [
        {
          "stepId": "pasta",
          "name": "Pasta",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "replicates": { "count": 3 }
        }
      ]
    },
    {
      "trackId": "plating",
      "name": "Plating",
      "steps": [
        {
          "stepId": "plate",
          "name": "Plate",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "logic": "all",
            "triggers": [
              { "type": "afterStep", "stepId": "sauce", "instances": "each" },
              { "type": "afterStep", "stepId": "pasta", "instances": "each" }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Give the upstream steps the same replicates.count.

Python. Python reports E_EACH_COUNT_MISMATCH.

E_INFLIGHT_GT_COUNT

Error. replicates.maxInFlight is greater than replicates.count.

maxInFlight caps how many instances are in progress at once; it cannot exceed the number of instances.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "mix",
          "name": "Mix",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "bake",
          "name": "Bake",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "mix" },
          "replicates": { "count": 3, "maxInFlight": 5 }
        },
        {
          "stepId": "cool",
          "name": "Cool",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "bake", "instances": "each" }
        },
        {
          "stepId": "box",
          "name": "Box",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "cool", "instances": "all" }
        }
      ]
    }
  ]
}

Fix. Set maxInFlight to at most count, or omit it.

Python. Python reports E_INFLIGHT_GT_COUNT.

E_INFLIGHT_NO_CHAIN

Error. maxInFlight on a serial replicate with no "each" descendants has no effect.

Serial instances never overlap, and with no per-instance steps after them an instance is done as soon as it ends, so the cap never holds one back.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "mix",
          "name": "Mix",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "bake",
          "name": "Bake",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "mix" },
          "replicates": { "count": 3, "mode": "serial", "maxInFlight": 2 }
        },
        {
          "stepId": "box",
          "name": "Box",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "bake", "instances": "all" }
        }
      ]
    }
  ]
}

Fix. Remove maxInFlight, or chain a per-instance step after it with instances: "each".

Python. Python reports E_INFLIGHT_NO_CHAIN.

W_UNBARRIERED_CHAIN

Warning. An instances: "each" chain has no "all" barrier, but later steps do not wait for it.

The per-instance chain is never joined, so nothing waits for every instance to finish while other work carries on. Often a missing barrier step.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "cookies",
      "name": "Cookies",
      "steps": [
        {
          "stepId": "mix",
          "name": "Mix",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "bake",
          "name": "Bake",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "mix" },
          "replicates": { "count": 3, "mode": "serial" }
        },
        {
          "stepId": "cool",
          "name": "Cool",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "bake", "instances": "each" }
        }
      ]
    },
    {
      "trackId": "cleanup",
      "name": "Cleanup",
      "steps": [
        {
          "stepId": "wipe",
          "name": "Wipe",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStartOffset", "offsetSeconds": 3600 }
        }
      ]
    }
  ]
}

Fix. Add a step with instances: "all" on the chain's last step if later work should wait for every instance.

Python. Python reports W_UNBARRIERED_CHAIN.

I_IMPLICIT_BARRIER

Info. A 0.3.0 program waits for a replicated step without saying instances.

A reference to a replicated step with no instances waits for every instance (an implicit "all" barrier), as in 0.2.0. In 0.3.0-alpha programs the note suggests saying so.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "mix",
          "name": "Mix",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" }
        },
        {
          "stepId": "bake",
          "name": "Bake",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "mix" },
          "replicates": { "count": 3, "mode": "serial" }
        },
        {
          "stepId": "box",
          "name": "Box",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "afterStep", "stepId": "bake" }
        }
      ]
    }
  ]
}

Fix. Add instances: "all" to make the barrier explicit, or "each" to run the step once per instance.

Python. Python reports nothing for it. JS only (PRD per-instance triggers §9 Q5).

expansion_failed

Error. Replicates could not be expanded.

The replicate expander rejected the program; the message says why. The one case the checks before it do not catch: instances: "all" or "any" inside a compound trigger of the other logic, which would need a nested compound. The program is then schema-checked as written.

Example. Merged over the base program:

{
  "schemaVersion": "0.3.0-alpha",
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "bake",
          "name": "Bake",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "replicates": { "count": 2 }
        },
        {
          "stepId": "box",
          "name": "Box",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": {
            "logic": "all",
            "triggers": [
              { "type": "afterStep", "stepId": "bake", "instances": "any" },
              { "type": "programStart" }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Check replicates and instances on the steps the message names; match the compound's logic to instances or split the trigger.

Python. rhylthyme validate reports nothing; validate_program_structured reports expansion_failed. rhylthyme validate does not expand replicates, so it has nothing to fail; validate_program_structured does.

Alerts

Step alerts (offsets before or after a step's start or end).

Code Severity Summary Python
E_ALERT_BAD_OFFSET error An alert's offsetSeconds is not a number or time string. E_ALERT_BAD_OFFSET
W_ALERT_BEFORE_MANUAL_START warning An alert before the start of a manually started step never fires. W_ALERT_BEFORE_MANUAL_START
W_ALERT_BEFORE_PROGRAM_START warning An alert would fire before the program starts, so it never fires. W_ALERT_BEFORE_PROGRAM_START
W_ALERT_BEFORE_INDEFINITE_END warning An alert before the end of an indefinite step never fires. W_ALERT_BEFORE_INDEFINITE_END

E_ALERT_BAD_OFFSET

Error. An alert's offsetSeconds is not a number or time string.

offsetSeconds is signed seconds or a signed time string ("-2m", "30s", "1h30m") relative to the step's start or end.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "alerts": [{ "event": "end", "offsetSeconds": "a bit before" }]
        }
      ]
    }
  ]
}

Fix. Use seconds (-120) or a signed time string ("-2m").

Python. Python reports E_ALERT_BAD_OFFSET.

W_ALERT_BEFORE_MANUAL_START

Warning. An alert before the start of a manually started step never fires.

A manual step's start is not known until someone starts it, so an alert timed before it has no anchor.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "manual" },
          "alerts": [{ "event": "start", "offsetSeconds": "-1m" }]
        }
      ]
    }
  ]
}

Fix. Anchor the alert on the end of the step before, or use offsetSeconds >= 0.

Python. Python reports W_ALERT_BEFORE_MANUAL_START.

W_ALERT_BEFORE_PROGRAM_START

Warning. An alert would fire before the program starts, so it never fires.

The step starts at (or soon after) the program start and the alert's negative offset reaches further back than that.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "alerts": [{ "event": "start", "offsetSeconds": -300 }]
        }
      ]
    }
  ]
}

Fix. Shorten the offset, or delay the step with programStartOffset.

Python. Python reports W_ALERT_BEFORE_PROGRAM_START.

W_ALERT_BEFORE_INDEFINITE_END

Warning. An alert before the end of an indefinite step never fires.

An indefinite step has no projected end, so an alert timed before it has no anchor.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "prep",
          "name": "Prep",
          "duration": { "type": "indefinite", "defaultSeconds": 900 },
          "startTrigger": { "type": "programStart" },
          "alerts": [{ "event": "end", "offsetSeconds": "-2m" }]
        }
      ]
    }
  ]
}

Fix. Use offsetSeconds >= 0 on event "end", or give the step a fixed or variable duration.

Python. Python reports W_ALERT_BEFORE_INDEFINITE_END.

Instruments

Instrument steps: galago-tools commands and LabMCP server calls.

Code Severity Summary Python
I_INSTRUMENT_DURATION info An instrument step with no duration ends when its call replies; timings use an estimate. no
instrument_bad_shape error A step's instrument block is malformed. schema_error
instrument_no_end warning An instrument step has no command, no until and no duration: only the operator can end it. instrument_no_end (CLI), none (library)
instrument_unchecked warning Instrument calls were not checked. instrument_unchecked (CLI), none (library)
instrument_unknown_tool_type error, warning An instrument's toolType is not a known galago tool type or LabMCP server. instrument_unknown_tool_type (CLI), logic_error (library)
instrument_invalid_command error An instrument call is not valid for its tool type. instrument_invalid_command (CLI), logic_error (library)
instrument_over_limit error A LabMCP call's parameter is beyond the server's default limit. instrument_over_limit (CLI), logic_error (library)
instrument_returns_early error A LabMCP call that replies as soon as it starts is used to end a step. instrument_returns_early (CLI), logic_error (library)
instrument_dose_outlasts_step warning A pump dose started by a step takes longer than the step. instrument_dose_outlasts_step (CLI), none (library)
instrument_catalog_not_loaded warning A LabMCP step was not checked because the LabMCP catalogue is not loaded. no

I_INSTRUMENT_DURATION

Info. An instrument step with no duration ends when its call replies; timings use an estimate.

The step's command (or until call) ends it, so it needs no duration. For planning, the validator and analyzer time it by an estimate from its params (e.g. a shake's duration, a LabMCP series' count x interval) or a default, and say which.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "shake",
          "name": "Shake",
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "toolType": "bioshake",
            "command": "start_shake",
            "params": { "speed": 1000, "duration": 300 }
          }
        }
      ]
    }
  ]
}

Fix. Nothing to fix. rhylthyme plan --workcell writes the tool's own estimate into the program.

Python. Python reports nothing for it.

instrument_bad_shape

Error. A step's instrument block is malformed.

The instrument has a missing or empty tool, both command and until, no call or actions at all, an empty start/end/onAbort list, an action without a command, an unknown key or a bad timeoutSeconds. The schema reports the same problem (schema_*), with the program path; this finding says it in the instrument's own terms. The step's calls are not checked further.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "shake",
          "name": "Shake",
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "toolType": "bioshake",
            "command": "start_shake",
            "params": { "speed": 1000, "duration": 300 },
            "start": []
          }
        }
      ]
    }
  ]
}

Also emits schema_minItems.

Fix. Use {"tool": "shaker", "command": "start_shake", "params": {...}}, or start/until/end actions: {"command", "params", "tool"}.

Python. Python reports schema_error. Python reports a malformed instrument through its schema check only.

instrument_no_end

Warning. An instrument step has no command, no until and no duration: only the operator can end it.

Its start actions run and nothing ends the step on its own. Timings use a default.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "shake",
          "name": "Shake",
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "toolType": "bioshake",
            "start": [
              { "command": "start_shake", "params": { "speed": 1000, "duration": 300 } }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Add a duration, or an until action whose reply ends the step.

Python. rhylthyme validate reports instrument_no_end; validate_program_structured reports nothing. validate_program_structured keeps only the errors of the instrument checks.

instrument_unchecked

Warning. Instrument calls were not checked.

The step's instrument has no toolType, or some of its actions go to another tool, so the commands cannot be checked without the lab's workcell file.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "shake",
          "name": "Shake",
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "command": "start_shake",
            "params": { "speed": 1000, "duration": 300 }
          }
        }
      ]
    }
  ]
}

Fix. Add toolType (the galago tool type, e.g. "bioshake", or a LabMCP package such as "labmcp-ika"), or run rhylthyme validate --workcell.

Python. rhylthyme validate reports instrument_unchecked; validate_program_structured reports nothing. Python reports it once per program ("Instrument steps were not checked") when the driver package is missing, and as an error with --workcell. validate_program_structured keeps only the errors of the instrument checks.

instrument_unknown_tool_type

Error or Warning. An instrument's toolType is not a known galago tool type or LabMCP server.

An unknown galago toolType is an error. A toolType starting with "labmcp-" that is not in the LabMCP catalogue is a warning: the server may be newer than the catalogue, so its calls are just not checked.

Example 1 (error). Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "shake",
          "name": "Shake",
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "toolType": "blender",
            "command": "start_shake"
          }
        }
      ]
    }
  ]
}

Example 2 (warning). Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "heat",
          "name": "Heat",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "stirrer",
            "toolType": "labmcp-nope",
            "command": "stop_heating"
          }
        }
      ]
    }
  ]
}

Fix. Use a galago tool type the message lists, or a LabMCP package name from the catalogue.

Python. rhylthyme validate reports instrument_unknown_tool_type; validate_program_structured reports logic_error. Needs rhylthyme-galago / rhylthyme-labmcp. validate_program_structured keeps only the errors of the instrument checks, as logic_error.

instrument_invalid_command

Error. An instrument call is not valid for its tool type.

The command does not exist for the toolType, or its params do not match the command's parameters (unknown, missing or of the wrong type). galago tool types are checked against galago-tools' commands, LabMCP servers against the LabMCP catalogue.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "shake",
          "name": "Shake",
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "shaker",
            "toolType": "bioshake",
            "command": "start_shake",
            "params": { "rpm": 1000 }
          }
        }
      ]
    }
  ]
}

Fix. Use a command and params the message lists for that tool type.

Python. rhylthyme validate reports instrument_invalid_command; validate_program_structured reports logic_error. Needs rhylthyme-galago. From the rhylthyme-galago / rhylthyme-labmcp driver packages, when installed. validate_program_structured reports it as logic_error.

instrument_over_limit

Error. A LabMCP call's parameter is beyond the server's default limit.

LabMCP servers refuse values past their safety limits (a maximum temperature, a series length, ...). The check uses each server's default limits; a lab may configure others.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "heat",
          "name": "Heat",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "stirrer",
            "toolType": "labmcp-ika",
            "start": [
              { "command": "set_temperature", "params": { "temperature_c": 200 } }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Use a value within the limit the message names, or raise the limit in the server's configuration.

Python. rhylthyme validate reports instrument_over_limit; validate_program_structured reports logic_error. Needs rhylthyme-labmcp. From rhylthyme-labmcp, when installed. validate_program_structured reports it as logic_error.

instrument_returns_early

Error. A LabMCP call that replies as soon as it starts is used to end a step.

Some calls (a syringe pump's infuse and withdraw) reply when the work starts, not when it is done; as the step's command or until call they would end the step at once.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "dose",
          "name": "Dose",
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "pump",
            "toolType": "labmcp-new-era",
            "command": "infuse",
            "params": { "volume_ml": 1, "rate_ml_min": 1 }
          }
        }
      ]
    }
  ]
}

Fix. Send it as a start action and give the step a duration at least as long as the dose.

Python. rhylthyme validate reports instrument_returns_early; validate_program_structured reports logic_error. Needs rhylthyme-labmcp. From rhylthyme-labmcp, when installed. validate_program_structured reports it as logic_error.

instrument_dose_outlasts_step

Warning. A pump dose started by a step takes longer than the step.

The dose's volume / rate is longer than the step's duration, so its end actions and the next steps start while the pump still runs.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "dose",
          "name": "Dose",
          "duration": { "type": "fixed", "seconds": 10 },
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "pump",
            "toolType": "labmcp-new-era",
            "start": [
              { "command": "infuse", "params": { "volume_ml": 1, "rate_ml_min": 1 } }
            ]
          }
        }
      ]
    }
  ]
}

Fix. Lengthen the step to at least the dose time, or lower the volume or raise the rate.

Python. rhylthyme validate reports instrument_dose_outlasts_step; validate_program_structured reports nothing. Needs rhylthyme-labmcp. From rhylthyme-labmcp, when installed. validate_program_structured keeps only the errors of the instrument checks.

instrument_catalog_not_loaded

Warning. A LabMCP step was not checked because the LabMCP catalogue is not loaded.

The browser entry of @rhylthyme/timeline/schedule (and browser/schedule.js) leaves out the LabMCP catalogue (about 350 KB). Until it is registered, each LabMCP step gets this warning instead of its checks. The Node entry always has it.

Example. Merged over the base program:

{
  "tracks": [
    {
      "trackId": "main",
      "name": "Main",
      "steps": [
        {
          "stepId": "heat",
          "name": "Heat",
          "duration": { "type": "fixed", "seconds": 300 },
          "startTrigger": { "type": "programStart" },
          "instrument": {
            "tool": "stirrer",
            "toolType": "labmcp-ika",
            "start": [
              { "command": "set_temperature", "params": { "temperature_c": 40 } }
            ]
          }
        }
      ]
    }
  ]
}

Validated with the browser entry before the LabMCP catalogue is registered.

Fix. import '@rhylthyme/timeline/schedule/labmcp-catalog' (or add browser/labmcp-catalog.js to the page) before validating.

Python. Python reports nothing for it. Python always has its catalogue.