Skip to content

feat(schedules): add exact skip and unskip actions - #357

Merged
jromualdez-scale merged 5 commits into
mainfrom
jerome/schedule-skip-unskip-backend
Jul 10, 2026
Merged

feat(schedules): add exact skip and unskip actions#357
jromualdez-scale merged 5 commits into
mainfrom
jerome/schedule-skip-unskip-backend

Conversation

@jromualdez-scale

@jromualdez-scale jromualdez-scale commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add backend APIs to skip and unskip a specific run schedule occurrence.
  • Store one-off skips in Temporal schedule exclusions and expose future skipped action times in schedule responses.
  • Require clients to pass an explicit timezone-aware scheduled_time for both skip and unskip.
  • Validate skip targets against Temporal's upcoming action times, and validate unskip targets against currently skipped future times.
  • Prune expired exact one-off skips when skip/unskip mutates Temporal state, while preserving broader recurring skip specs.
  • Keep this PR backend-only: no UI changes and no task-count logic.

Test plan

  • uv run python scripts/generate_openapi_spec.py
  • uv run ruff format src/adapters/temporal/adapter_temporal.py src/adapters/temporal/port.py src/api/routes/agent_run_schedules.py src/api/schemas/agent_run_schedules.py src/domain/services/agent_run_schedule_service.py src/domain/use_cases/agent_run_schedules_use_case.py tests/unit/api/test_agent_run_schedules_authz.py tests/unit/services/test_agent_run_schedule_service.py tests/unit/adapters/test_temporal_schedule_skips.py
  • uv run ruff check src/adapters/temporal/adapter_temporal.py src/adapters/temporal/port.py src/api/routes/agent_run_schedules.py src/api/schemas/agent_run_schedules.py src/domain/services/agent_run_schedule_service.py src/domain/use_cases/agent_run_schedules_use_case.py tests/unit/api/test_agent_run_schedules_authz.py tests/unit/services/test_agent_run_schedule_service.py tests/unit/adapters/test_temporal_schedule_skips.py
  • uv run python -m py_compile src/adapters/temporal/adapter_temporal.py src/adapters/temporal/port.py src/api/routes/agent_run_schedules.py src/api/schemas/agent_run_schedules.py src/domain/services/agent_run_schedule_service.py src/domain/use_cases/agent_run_schedules_use_case.py tests/unit/api/test_agent_run_schedules_authz.py tests/unit/services/test_agent_run_schedule_service.py tests/unit/adapters/test_temporal_schedule_skips.py
  • uv run pytest tests/unit/api/test_agent_run_schedules_authz.py tests/unit/services/test_agent_run_schedule_service.py tests/unit/adapters/test_temporal_schedule_skips.py (blocked locally: missing testcontainers during test collection)

Greptile Summary

This PR adds backend-only skip and unskip endpoints for Temporal schedule occurrences, storing one-off exclusions as ScheduleCalendarSpec entries in the schedule's spec.skip list and surfacing them through a new skipped_action_times field on the response schema.

  • New adapter logic: skip_schedule_action and unskip_schedule_action validate the target time against Temporal's upcoming or currently-skipped action times, then apply the mutation atomically via handle.update(_apply); a family of static helpers (_one_off_skip_spec, _same_one_off_skip, _without_past_one_off_skips, extract_one_off_skip_times) encodes and decodes exact-instant CalendarSpec entries and prunes expired ones on every write.
  • API layer: Two new POST endpoints (/{schedule_id}/skip and /{schedule_id}/unskip) require a timezone-aware scheduled_time validated at schema parse time, and both use AuthorizedOperationType.update consistent with other mutating endpoints.
  • Tests: Unit tests cover static-helper round-trips (UTC and named-timezone), broad-spec preservation, cross-timezone instant matching, and authz/service-layer delegation; all use fixed datetimes making them deterministic.

Confidence Score: 4/5

Safe to merge as a backend-only change with no data-loss risk; the skip/unskip correctness depends on Temporal's optimistic-concurrency update semantics, and a narrow race window between the pre-validation describe and the commit remains in the adapter.

The adapter's skip_schedule_action validates scheduled_time against next_action_times from an initial handle.describe(), then applies the mutation in a separate handle.update(_apply) callback. If the schedule fires in the window between those two calls, the pre-validation passes on a stale snapshot and the skip is added for a time that already executed. The guard inside _apply uses a now captured before the update call, so it won't catch the race. All other logic — deduplication, expired-skip pruning, timezone round-trip, authz — is solid and well-tested.

agentex/src/adapters/temporal/adapter_temporal.py — the pre-validate / commit window in skip_schedule_action and the stale now guard inside the _apply closure.

Important Files Changed

Filename Overview
agentex/src/adapters/temporal/adapter_temporal.py Adds skip_schedule_action, unskip_schedule_action, and a family of static helpers; the pre-validate / commit design has a previously-flagged TOCTOU risk that remains unaddressed, though the explicit scheduled_time requirement reduces the blast radius.
agentex/src/adapters/temporal/port.py Adds skip_schedule_action and unskip_schedule_action abstract methods to TemporalGateway; extract_one_off_skip_times is not added to the port, still violating port isolation as noted in a prior review.
agentex/src/domain/services/agent_run_schedule_service.py Adds skip_schedule_action / unskip_schedule_action service methods and populates skipped_action_times in _extract_live_fields; still calls TemporalAdapter.extract_one_off_skip_times as a concrete static (previously flagged).
agentex/src/api/routes/agent_run_schedules.py Adds POST /{schedule_id}/skip and /{schedule_id}/unskip endpoints; both correctly use AuthorizedOperationType.update, consistent with other mutating schedule endpoints.
agentex/src/api/schemas/agent_run_schedules.py Adds SkipRunScheduleRequest / UnskipRunScheduleRequest with required timezone-aware scheduled_time validated via model_validator, and adds skipped_action_times to AgentRunScheduleResponse.
agentex/tests/unit/adapters/test_temporal_schedule_skips.py Good unit-test coverage of the static helpers: round-trip, timezone handling, broad-spec preservation, cross-timezone instant matching, and past-time rejection; all tests use fixed datetimes so they remain deterministic.
agentex/tests/unit/api/test_agent_run_schedules_authz.py Adds authz tests confirming skip and unskip routes enforce AuthorizedOperationType.update; consistent pattern with existing pause/resume/trigger tests.
agentex/tests/unit/services/test_agent_run_schedule_service.py Adds service-layer tests for skip/unskip delegation and schema validation; missing a symmetric test for UnskipRunScheduleRequest() without scheduled_time, though the parametrized timezone test covers both types.
agentex/openapi.yaml Adds skip and unskip path items plus SkipRunScheduleRequest, UnskipRunScheduleRequest schemas and skipped_action_times field; generated from source, consistent with code changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Route as API Route
    participant UseCase as AgentRunSchedulesUseCase
    participant Service as AgentRunScheduleService
    participant Adapter as TemporalAdapter
    participant Temporal as Temporal Server

    Client->>Route: "POST /{schedule_id}/skip {scheduled_time}"
    Route->>Route: _check_schedule_or_collapse_to_404 (AuthorizedOperationType.update)
    Route->>UseCase: skip_schedule_action(agent_id, schedule_id, scheduled_time)
    UseCase->>Service: skip_schedule_action(agent_id, schedule_id, scheduled_time)
    Service->>Service: get_by_agent_id_and_id_or_raise(agent_id, schedule_id)
    Service->>Adapter: skip_schedule_action(temporal_id, scheduled_time)
    Adapter->>Temporal: handle.describe()
    Temporal-->>Adapter: ScheduleDescription (next_action_times, spec.skip)
    Adapter->>Adapter: _validate_future_scheduled_time()
    Adapter->>Adapter: _contains_instant(scheduled_time, next_action_times)
    Adapter->>Adapter: _one_off_skip_spec(scheduled_time, time_zone_name)
    Adapter->>Temporal: handle.update(_apply)
    Note over Adapter,Temporal: _apply: prune past one-off skips, dedup, append new CalendarSpec
    Temporal-->>Adapter: OK
    Adapter-->>Service: None
    Service->>Adapter: describe_schedule(temporal_id)
    Temporal-->>Adapter: ScheduleDescription
    Adapter-->>Service: ScheduleDescription
    Service->>Service: _extract_live_fields() → skipped_action_times
    Service-->>Route: AgentRunScheduleResponse
    Route-->>Client: 200 AgentRunScheduleResponse (with skipped_action_times)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Route as API Route
    participant UseCase as AgentRunSchedulesUseCase
    participant Service as AgentRunScheduleService
    participant Adapter as TemporalAdapter
    participant Temporal as Temporal Server

    Client->>Route: "POST /{schedule_id}/skip {scheduled_time}"
    Route->>Route: _check_schedule_or_collapse_to_404 (AuthorizedOperationType.update)
    Route->>UseCase: skip_schedule_action(agent_id, schedule_id, scheduled_time)
    UseCase->>Service: skip_schedule_action(agent_id, schedule_id, scheduled_time)
    Service->>Service: get_by_agent_id_and_id_or_raise(agent_id, schedule_id)
    Service->>Adapter: skip_schedule_action(temporal_id, scheduled_time)
    Adapter->>Temporal: handle.describe()
    Temporal-->>Adapter: ScheduleDescription (next_action_times, spec.skip)
    Adapter->>Adapter: _validate_future_scheduled_time()
    Adapter->>Adapter: _contains_instant(scheduled_time, next_action_times)
    Adapter->>Adapter: _one_off_skip_spec(scheduled_time, time_zone_name)
    Adapter->>Temporal: handle.update(_apply)
    Note over Adapter,Temporal: _apply: prune past one-off skips, dedup, append new CalendarSpec
    Temporal-->>Adapter: OK
    Adapter-->>Service: None
    Service->>Adapter: describe_schedule(temporal_id)
    Temporal-->>Adapter: ScheduleDescription
    Adapter-->>Service: ScheduleDescription
    Service->>Service: _extract_live_fields() → skipped_action_times
    Service-->>Route: AgentRunScheduleResponse
    Route-->>Client: 200 AgentRunScheduleResponse (with skipped_action_times)
Loading

Reviews (4): Last reviewed commit: "Merge branch 'main' into jerome/schedule..." | Re-trigger Greptile

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

✱ Stainless preview builds

This PR will update the agentex-sdk SDKs with the following commit messages.

openapi

feat(api): add skip/unskip methods and skipped_action_times to schedules

python

feat(api): add skipped_action_times field to agents schedule responses

typescript

feat(api): add skipped_action_times field to schedules responses
agentex-sdk-openapi studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅

⚠️ agentex-sdk-typescript studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/agentex-sdk-typescript/c248788ca53dab69de8ac6680d01fdcd73566d6f/dist.tar.gz
⚠️ agentex-sdk-python studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ✅lint ✅test ✅

pip install https://pkg.stainless.com/s/agentex-sdk-python/de49d43d6dd4fd12896c519ebd745c87f224f596/agentex_client-0.17.0-py3-none-any.whl

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-07-10 16:56:02 UTC

@jromualdez-scale jromualdez-scale changed the title Add exact skip and unskip actions for run schedules feat(schedules): add exact skip and unskip actions Jul 10, 2026
@jromualdez-scale
jromualdez-scale marked this pull request as ready for review July 10, 2026 14:41
@jromualdez-scale
jromualdez-scale requested a review from a team as a code owner July 10, 2026 14:41
@jromualdez-scale
jromualdez-scale marked this pull request as draft July 10, 2026 14:41
Comment thread agentex/src/domain/services/agent_run_schedule_service.py Outdated
Comment thread agentex/src/adapters/temporal/adapter_temporal.py
jromualdez-scale and others added 2 commits July 10, 2026 10:55
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jromualdez-scale
jromualdez-scale marked this pull request as ready for review July 10, 2026 15:11
@rpatel-scale

Copy link
Copy Markdown
Contributor

Curious, is there a customer need around this or are we pre-emptively implementing this?

)

@model_validator(mode="after")
def require_scheduled_time_timezone(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate that time is in the future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just checking that the passed in time includes timezone information

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a non-future time would be a no-op in worst case and we have a clean up for accumulated past times

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah but would prefer to have stronger validation here (especially now that i read only exact times are provided). Returning a 200 for a 11:00 request OR a request in the past would mean that server accepted my request. Would prefer to through a validation error if invalid time (no scheduled run at that time OR past run)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add this in. What do you think of the following:
For skip:

  • Reject if scheduled_time is in the past.
  • Reject if scheduled_time is not one of Temporal’s upcoming next_action_times.
  • Otherwise add the one-off skip.

For unskip:

  • Reject if scheduled_time is in the past.
  • Reject if scheduled_time is not currently in skipped_action_times.
  • Otherwise remove that one-off skip.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect sounds good. Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated @rpatel-scale !

class SkipRunScheduleRequest(BaseModel):
scheduled_time: datetime = Field(
...,
description="Specific scheduled fire time to skip.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would this work if lets say I have agent configured to run at 12:00 each day and I say pass in skip request for 11:00? Will it skip the 12:00 run?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will not skip the 12:00 run it skips only the exact time provided. There is a separate "snooze" ability that will skip all runs in a specified period

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attached a screenshot below.. on the client side, we’ll always know the exact occurrence the user is trying to skip because the UI renders concrete upcoming run times

@jromualdez-scale

jromualdez-scale commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Curious, is there a customer need around this or are we pre-emptively implementing this?

@rpatel-scale it is preemptive but motivated by the UI/UX designs we have mocked up for this feature

For some context here is a rough mock up I have been working on
Screenshot 2026-07-09 at 11 12 48 AM

@jromualdez-scale
jromualdez-scale merged commit ac88134 into main Jul 10, 2026
32 checks passed
@jromualdez-scale
jromualdez-scale deleted the jerome/schedule-skip-unskip-backend branch July 10, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants