---
title: "Ticket lifecycle sweeps, dry run by default (GET)"
method: GET
path: "/cron/stale-in-progress"
tags: ["Cron"]
---

# Ticket lifecycle sweeps, dry run by default (GET)

`GET /cron/stale-in-progress`

## Request

- Base URL: `https://api.apphandoff.com/api`
- URL: `https://api.apphandoff.com/api/cron/stale-in-progress`
- Auth: none required

The hourly ticket-lifecycle tick (HO-15546). Runs, in order and under one shared
~75s deadline: the closing-line close (a parked closing-line ticket closes once its
deploy is recorded, or once its merge is on the default branch when its repo
reports no deploys; never on elapsed time), the in-progress move-back (no agent for
48h returns the ticket to up next) and the 30-day archive (an untouched backlog or
validation ticket is archived). Also trims the agent_notifications catch-up log to
14 days.
DRY RUN BY DEFAULT: the sweeps only count and sample unless `dry_run=0` is sent
explicitly; any other value, or none, stays dry. Each sweep result carries
`scanned`/`acted`/`skipped`/`errors`, a `truncated` flag and up to 25 sample ticket
ids. A truncation is not a failure; top-level `ok` is the AND of the three sweeps
(the retention sweep is best-effort and never affects it). One sweep throwing sets
`ok: false` and does not stop the others.
Protected by CRON_SECRET via the x-cron-secret header ONLY — a token
query parameter is rejected (secrets do not belong in URLs).

## Query parameters

- `dry_run` '0' | '1'

## Response `200`

Lifecycle sweep result

- CronStaleInProgressResult
  - `ok` boolean, required: AND of the three sweeps' `ok`. The best-effort retention sweep never affects it.
  - `dry_run` boolean, required: True unless the caller sent an explicit `dry_run=0`.
  - `closing_line_close` CronLifecycleSweepResult, required
    - `ok` boolean, required: False when a read the sweep depends on failed, or the sweep threw. A truncation never sets it.
    - `dryRun` boolean, required
    - `scanned` integer, required: Candidate rows read.
    - `acted` integer, required: Tickets closed, moved or archived (in a dry run, the tickets that would be).
    - `skipped` integer, required: Eligible tickets left alone — a lost race, a refused write, or a read that failed closed.
    - `errors` integer, required
    - `truncated` boolean, required: True when the per-tick cap, the shared deadline or the scan cap stopped the sweep before every candidate was seen.
    - `samples` object[], required: Up to 25 tickets the sweep acted on or skipped.
      - `ticketId` string, required
      - `number` integer, nullable, required
      - `action` string, required: closed | would_close | returned | would_return | archived | would_archive | skipped
      - `reason` string
  - `in_progress_return` CronLifecycleSweepResult, required
    - `ok` boolean, required: False when a read the sweep depends on failed, or the sweep threw. A truncation never sets it.
    - `dryRun` boolean, required
    - `scanned` integer, required: Candidate rows read.
    - `acted` integer, required: Tickets closed, moved or archived (in a dry run, the tickets that would be).
    - `skipped` integer, required: Eligible tickets left alone — a lost race, a refused write, or a read that failed closed.
    - `errors` integer, required
    - `truncated` boolean, required: True when the per-tick cap, the shared deadline or the scan cap stopped the sweep before every candidate was seen.
    - `samples` object[], required: Up to 25 tickets the sweep acted on or skipped.
      - `ticketId` string, required
      - `number` integer, nullable, required
      - `action` string, required: closed | would_close | returned | would_return | archived | would_archive | skipped
      - `reason` string
  - `stale_archive` CronLifecycleSweepResult, required
    - `ok` boolean, required: False when a read the sweep depends on failed, or the sweep threw. A truncation never sets it.
    - `dryRun` boolean, required
    - `scanned` integer, required: Candidate rows read.
    - `acted` integer, required: Tickets closed, moved or archived (in a dry run, the tickets that would be).
    - `skipped` integer, required: Eligible tickets left alone — a lost race, a refused write, or a read that failed closed.
    - `errors` integer, required
    - `truncated` boolean, required: True when the per-tick cap, the shared deadline or the scan cap stopped the sweep before every candidate was seen.
    - `samples` object[], required: Up to 25 tickets the sweep acted on or skipped.
      - `ticketId` string, required
      - `number` integer, nullable, required
      - `action` string, required: closed | would_close | returned | would_return | archived | would_archive | skipped
      - `reason` string
  - `agent_notification_retention` object, required
    - `ok` boolean, required

## Other responses

- `401`: Missing or invalid CRON_SECRET

---

[API](https://skmtc.dev/apphandoff/apis/apphandoff-api.md) · [All operations](https://skmtc.dev/apphandoff/apis/apphandoff-api/llms.txt) · [OpenAPI document](https://skmtc.dev/apphandoff/apis/apphandoff-api/revisions/9f138fe46d56?raw)
