Skip to content
Open
1 change: 1 addition & 0 deletions docs/configuration/tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Built-in tools are included with docker-agent and require no external dependenci
| `filesystem` | Read, write, list, search, navigate | [Filesystem](../../tools/filesystem/index.md) |
| `shell` | Execute shell commands synchronously | [Shell](../../tools/shell/index.md) |
| `background_jobs` | Run and manage long-running shell commands | [Background Jobs](../../tools/background-jobs/index.md) |
| `scheduler` | Schedule instructions to run at a time or on a recurring interval | [Scheduler](../../tools/scheduler/index.md) |
| `think` | Reasoning scratchpad | [Think](../../tools/think/index.md) |
| `plan` | Shared persistent scratchpad for multi-agent collaboration | [Plan](../../tools/plan/index.md) |
| `session_plan` | Per-session markdown plan for the draft-review-execute workflow | [Session Plan](../../tools/session_plan/index.md) |
Expand Down
93 changes: 93 additions & 0 deletions docs/tools/scheduler/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: "Scheduler Tool"
description: "Schedule instructions to run at a time or on a recurring interval."
keywords: docker agent, ai agents, tools, toolsets, scheduler tool, cron
linkTitle: "Scheduler"
weight: 135
canonical: https://docs.docker.com/ai/docker-agent/tools/scheduler/
---

_Schedule instructions to run at a time or on a recurring interval._

## Overview

The scheduler toolset lets an agent make something happen at a chosen time or on a repeating cadence during a session. You give it an instruction and a schedule; when the schedule is due, the instruction is delivered back to the agent, which then carries out the action with its normal tools (`shell`, `api`, `fetch`, and so on).

The scheduler does not run shell or API calls itself. When a schedule fires it injects the instruction into the agent loop via the runtime's recall mechanism — the same primitive [`background_jobs`](../background-jobs/index.md) uses to report completed work — and the agent decides how to act. This keeps every action under the agent's normal tools and permissions rather than adding a second, unattended
command runner.

> [!NOTE]
> Schedules only fire while the session is running (interactive TUI or a server mode) and are not persisted across restarts. Scheduling requires a host that supports recall; if it does not, `create_schedule` returns an error.

## Configuration

```yaml
toolsets:
- type: scheduler
```

No configuration options.

## Tools

| Tool | Description |
| --- | --- |
| `create_schedule` | Register an instruction to run at a time or interval. |
| `list_schedules` | List active schedules with their id, spec, and next fire time. |
| `cancel_schedule` | Remove a schedule by id. |

### `create_schedule`

| Parameter | Required | Description |
| --- | --- | --- |
| `prompt` | Yes | The instruction to deliver to the agent when the schedule fires. |
| `when` | Yes | When to fire (see [Schedule specs](#schedule-specs)). |
| `name` | No | Optional human-readable label. |

Returns the new schedule's id and its next fire time.

### `cancel_schedule`

| Parameter | Required | Description |
| --- | --- | --- |
| `id` | Yes | The id of the schedule to cancel (from `create_schedule` or `list_schedules`). |

## Schedule specs

The `when` argument accepts:

| Form | Meaning | Example |
| --- | --- | --- |
| `in:<duration>` | One-shot, after a delay | `in:10m` |
| `at:<RFC3339>` | One-shot, at an absolute future time | `at:2026-07-14T09:00:00Z` |
| `every:<duration>` | Recurring, at a fixed interval | `every:1h` |
| `minutely` / `hourly` / `daily` / `weekly` | Recurring preset intervals | `hourly` |

Durations use Go's duration syntax (`30s`, `15m`, `2h`). Preset and `every:` intervals are measured from the schedule's creation time (for example `hourly` fires every hour after it is created), not aligned to wall-clock slots.

## Example

```yaml
agents:
root:
model: openai/gpt-5-mini
description: A monitoring assistant
instruction: |
Every 15 minutes, run `git fetch` and tell me if origin/main moved.
toolsets:
- type: scheduler
- type: shell
```

The agent calls:

```text
create_schedule(prompt="Run git fetch and report if origin/main moved", when="every:15m")
```

Every 15 minutes it is reminded, runs the command with the `shell` tool, and reports back.

> [!TIP]
> **When to use**
>
> Use the scheduler for recurring monitoring, timed one-shots, and unattended housekeeping loops during a long-running session. For work that should run immediately and be awaited, use [`background_jobs`](../background-jobs/index.md) instead.
1 change: 1 addition & 0 deletions pkg/teamloader/toolsets/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var BuiltinToolsets = []BuiltinToolsetInfo{
builtinToolset("openapi", "openapi", "Generate tools automatically from an OpenAPI specification"),
builtinToolset("plan", "plan", "Shared persistent scratchpad for multi-agent collaboration"),
builtinToolset("rag", "rag", "Search document knowledge bases with hybrid retrieval (RAG)"),
builtinToolset("scheduler", "scheduler", "Schedule instructions to run at a time or on a recurring interval"),
builtinToolset("script", "script", "Define custom shell scripts as named tools with typed parameters"),
builtinToolset("session_context", "session_context", "Reference a previous session as context in the current one"),
builtinToolset("session_plan", "session_plan", "Per-session plan tracker for the draft/review/execute workflow"),
Expand Down
4 changes: 4 additions & 0 deletions pkg/teamloader/toolsets/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/docker/docker-agent/pkg/tools/builtin/openurl"
"github.com/docker/docker-agent/pkg/tools/builtin/plan"
"github.com/docker/docker-agent/pkg/tools/builtin/rag"
"github.com/docker/docker-agent/pkg/tools/builtin/scheduler"
"github.com/docker/docker-agent/pkg/tools/builtin/sessioncontext"
"github.com/docker/docker-agent/pkg/tools/builtin/sessionplan"
"github.com/docker/docker-agent/pkg/tools/builtin/shell"
Expand Down Expand Up @@ -58,6 +59,9 @@ func DefaultToolsetCreators() map[string]teamloader.ToolsetCreator {
"think": func(_ context.Context, _ latest.Toolset, _ string, _ *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return think.CreateToolSet()
},
"scheduler": func(_ context.Context, _ latest.Toolset, _ string, _ *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return scheduler.CreateToolSet()
},
"shell": func(ctx context.Context, toolset latest.Toolset, _ string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return shell.CreateToolSet(ctx, toolset, runConfig)
},
Expand Down
65 changes: 65 additions & 0 deletions pkg/tools/builtin/scheduler/schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package scheduler

import (
"fmt"
"strings"
"time"
)

var presets = map[string]time.Duration{
"minutely": time.Minute,
"hourly": time.Hour,
"daily": 24 * time.Hour,
"weekly": 7 * 24 * time.Hour,
}

func parseWhen(when string, now time.Time) (next time.Time, interval time.Duration, err error) {
s := strings.TrimSpace(when)
if s == "" {
return time.Time{}, 0, fmt.Errorf("empty schedule spec")
}

if d, ok := presets[strings.ToLower(s)]; ok {
return now.Add(d), d, nil
}

switch {
case hasPrefixFold(s, "in:"):
d, err := time.ParseDuration(strings.TrimSpace(s[len("in:"):]))
if err != nil {
return time.Time{}, 0, fmt.Errorf("invalid delay in %q: %w", when, err)
}
if d <= 0 {
return time.Time{}, 0, fmt.Errorf("delay must be positive in %q", when)
}
return now.Add(d), 0, nil

case hasPrefixFold(s, "every:"):
d, err := time.ParseDuration(strings.TrimSpace(s[len("every:"):]))
if err != nil {
return time.Time{}, 0, fmt.Errorf("invalid interval in %q: %w", when, err)
}
if d <= 0 {
return time.Time{}, 0, fmt.Errorf("interval must be positive in %q", when)
}
return now.Add(d), d, nil

case hasPrefixFold(s, "at:"):
ts, err := time.Parse(time.RFC3339, strings.TrimSpace(s[len("at:"):]))
if err != nil {
return time.Time{}, 0, fmt.Errorf("invalid RFC3339 time in %q: %w", when, err)
}
if !ts.After(now) {
return time.Time{}, 0, fmt.Errorf("scheduled time %s is not in the future", ts.Format(time.RFC3339))
}
return ts, 0, nil

default:
return time.Time{}, 0, fmt.Errorf(
"unrecognized schedule %q: use in:<dur>, at:<RFC3339>, every:<dur>, or minutely/hourly/daily/weekly", when)
}
}

func hasPrefixFold(s, prefix string) bool {
return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix)
}
50 changes: 50 additions & 0 deletions pkg/tools/builtin/scheduler/schedule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package scheduler

import (
"testing"
"time"

"github.com/stretchr/testify/require"
)

var testNow = time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)

func TestParseWhen(t *testing.T) {
t.Parallel()

tests := []struct {
name string
when string
wantNext time.Time
wantInterval time.Duration
wantErr bool
}{
{"delay", "in:10m", testNow.Add(10 * time.Minute), 0, false},
{"at rfc3339", "at:2026-07-13T15:00:00Z", time.Date(2026, 7, 13, 15, 0, 0, 0, time.UTC), 0, false},
{"every", "every:1h", testNow.Add(time.Hour), time.Hour, false},
{"minutely", "minutely", testNow.Add(time.Minute), time.Minute, false},
{"hourly", "hourly", testNow.Add(time.Hour), time.Hour, false},
{"daily", "daily", testNow.Add(24 * time.Hour), 24 * time.Hour, false},
{"weekly", "weekly", testNow.Add(7 * 24 * time.Hour), 7 * 24 * time.Hour, false},
{"whitespace and case", " Hourly ", testNow.Add(time.Hour), time.Hour, false},
{"empty", "", time.Time{}, 0, true},
{"garbage", "sometimes", time.Time{}, 0, true},
{"at in past", "at:2020-01-01T00:00:00Z", time.Time{}, 0, true},
{"every zero", "every:0s", time.Time{}, 0, true},
{"every negative", "every:-5m", time.Time{}, 0, true},
{"in bad duration", "in:abc", time.Time{}, 0, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
next, interval, err := parseWhen(tt.when, testNow)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantNext, next)
require.Equal(t, tt.wantInterval, interval)
})
}
}
Loading