Skip to content

Limits

What this plugin cannot do, stated plainly, because each of these is easier to learn here than from a workflow that silently does nothing.

Routine items are read-only

Apiary plugin sources implement none of the optional write capabilities. A routine's work item cannot be transitioned, labelled, commented on, or closed — there is nothing behind it to write to. An occurrence is an instant, not a record in a backing system.

Concretely, Apiary's config validation (SourceCapabilities) rejects a workflow pinned to this source that uses any of these — each verified against a live daemon:

Feature Rejected
on_complete / on_fail set_state yes
add_labels / assign_from_output yes
remove_labels yes
approval step with approvers / resume_on yes
wait_for with kind: ci yes (including nested in a parallel: group)
materialize: sub_issue yes

An operator-gate approval is allowed — one with a message and no approvers or resume_on. It is answered locally (apiary approvals, or the dashboard) and never reads a source signal, so it needs no write capability. That makes "run nightly, then wait for a human to sign off" a perfectly good routine.

A routine that needs to produce a ticket does it from inside the workflow. Have the agent emit APIARY_SPAWN and add a materialize: sub_issue step, so the ticket is created in a real source (GitHub, Jira) that does support write-back. The routine starts the work; a real source records it.

Always pin source: in a routine trigger

The capability check has two rules. A trigger that pins match.source: routines is checked against that source and fails validation — which is what you want. A trigger with no source pin is only rejected when no configured source supports the capability.

So in a hive that also runs GitHub or Jira, an unpinned routine workflow using set_state passes apiary validate and then silently does nothing at runtime, because the item it actually received came from this plugin. Verified against a live daemon:

Trigger on_complete.set_state Result
source: routines yes rejected at validate
unpinned, plugin is the only source yes rejected at validate
unpinned, GitHub also configured yes accepted, no-op at runtime

Pinning costs one line and turns a silent runtime no-op into a startup error.

The agent starts in /, so name your paths

Apiary dispatches every agent step with the process working directory hardcoded to / (internal/daemon/workflow.go, WorkingDir: "/"), and there is no config knob for it — see apiary#436.

An issue-driven agent rarely notices, because its soul file or prompt names the repository. A routine has nothing to name it. Observed on a real run: the agent found no git repository in /, searched $HOME for one, listed the operator's Documents folder along the way, and spent most of nine turns and $0.50 orienting before it found the right checkout.

So put the path in the prompt:

steps:
  - id: report
    agent: reporter
    prompt: |
      The repository is at /srv/projects/acme — cd there first.

      Report on its current health, READ ONLY …

Without it the agent will hunt, and it may find the wrong checkout and report on that instead — silently, because the output still looks right.

No overlap control

Each occurrence becomes a new internal task. Apiary's live-instance guard is per (task, workflow), so it does not apply across occurrences: a routine whose run takes longer than its interval will overlap itself.

The plugin cannot prevent this — it has no visibility into Apiary's state and, being a fresh process per poll, no memory of what it started.

Mitigations, in order of bluntness:

  1. Give the routine an interval comfortably longer than its worst-case run.
  2. Cap the agent's max_workers so overlapping runs queue rather than pile up.
  3. Have the workflow's first step check for its own prior run and exit early.

If you need real overlap: skip semantics, that has to live in the daemon, which knows what is running. It is out of scope for a source plugin.

No operator surface

There is no apiary routines command, no next-run column, and no dashboard tab. What you get:

  • What ran — the Tasks list, filtered by the routine:<id> label
  • What is scheduled — your config, and the state_file
  • What the plugin decided — its stderr, captured by the daemon per invocation (64 KiB) and logged against the plugin

Enabling or disabling a routine means editing config and restarting the daemon (enabled: false parks one without losing its cursor).

No auto-disable on repeated failure

The plugin emits occurrences; it never learns whether the resulting workflow succeeded. A routine whose workflow fails every night keeps firing every night.

Apiary's own settings.max_attempts caps consecutive failed instances per (task, workflow) — but every occurrence is a new task, so that cap resets each time and will not stop a chronically failing routine.

Watch for this with Apiary's notification channels on the workflow's on_fail.

Why once is not optional

Apiary's pre-dispatch guards drop a match when there is a live instance, when a once: true trigger has already completed, or when the failure cap is spent. None of them drop a match because the workflow completed earlier.

So if an occurrence item ever reappears — a restored state file, a hand-edited cursor, a state_file pointed at a fresh path — Apiary will dispatch the workflow again. The plugin's cursor is the first lock; trigger.once: true is the second, and the cheap one.

Not a general job runner

This schedules Apiary workflows. If what you want is "run this shell command every night", cron already does that better. The value here is that a routine enters the same pipeline as every other task: the same routing, the same agent dispatch, the same transcripts, instances, and history.