mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
fix(routines): include cronExpression and timezone in list trigger response (#3209)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Routines are recurring tasks that trigger agents on a schedule or
via webhook
> - Routine triggers store their schedule as a `cronExpression` +
`timezone` in the database
> - The `GET /companies/:companyId/routines` list endpoint is the
primary way API consumers (and the UI) discover all routines and their
triggers
> - But the list endpoint was silently dropping `cronExpression` and
`timezone` from each trigger object — the DB query fetched them, but the
explicit object-construction mapping only forwarded seven other fields
> - This PR fixes the mapping to include `cronExpression` and
`timezone`, and extends the `RoutineListItem.triggers` type to match
> - The benefit is that API consumers can now see the actual schedule
from the list endpoint, and future UI components reading from the list
cache will get accurate schedule data
## What Changed
- **`server/src/services/routines.ts`** — Added `cronExpression` and
`timezone` to the explicit trigger object mapping inside
`routinesService.list()`. The DB query (`listTriggersForRoutineIds`)
already fetched all columns via `SELECT *`; the values were being
discarded during object construction.
- **`packages/shared/src/types/routine.ts`** — Extended
`RoutineListItem.triggers` `Pick<RoutineTrigger, ...>` to include
`cronExpression` and `timezone` so the TypeScript type contract matches
the actual runtime shape.
- **`server/src/__tests__/routines-e2e.test.ts`** — Added assertions to
the existing schedule-trigger E2E test that verify both `cronExpression`
and `timezone` are present in the `GET /companies/:companyId/routines`
list response.
## Verification
```bash
# Run the route + service unit tests
npx vitest run server/src/__tests__/routines-routes.test.ts server/src/__tests__/routines-service.test.ts
# → 21 tests pass
# Confirm cronExpression appears in list response
curl /api/companies/{id}/routines | jq '.[].triggers[].cronExpression'
# → now returns the actual cron string instead of undefined
```
Manual reproduction per the issue:
1. Create a routine with a schedule trigger (`cronExpression: "47 14 * *
*"`, `timezone: "America/Mexico_City"`)
2. `GET /api/companies/{id}/routines` — trigger object now includes
`cronExpression` and `timezone`
## Risks
Low risk. The change only adds two fields to an existing response shape
— no fields removed, no behavior changed. The `cronExpression` is `null`
for non-schedule trigger kinds (webhook, etc.), consistent with
`RoutineTrigger.cronExpression: string | null`. No migration required.
## Model Used
- **Provider:** Anthropic
- **Model:** Claude Sonnet 4.6 (`claude-sonnet-4-6`)
- **Context window:** 200k tokens
- **Mode:** Extended thinking + tool use (agentic)
- Secondary adversarial review: OpenAI Codex (via codex-companion
plugin)
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (API-only fix; no UI rendering change)
- [ ] I have updated relevant documentation to reflect my changes (no
doc changes needed)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
@@ -129,7 +129,7 @@ export interface RoutineExecutionIssueOrigin {
|
||||
}
|
||||
|
||||
export interface RoutineListItem extends Routine {
|
||||
triggers: Pick<RoutineTrigger, "id" | "kind" | "label" | "enabled" | "nextRunAt" | "lastFiredAt" | "lastResult">[];
|
||||
triggers: Pick<RoutineTrigger, "id" | "kind" | "label" | "enabled" | "cronExpression" | "timezone" | "nextRunAt" | "lastFiredAt" | "lastResult">[];
|
||||
lastRun: RoutineRunSummary | null;
|
||||
activeIssue: RoutineIssueSummary | null;
|
||||
}
|
||||
|
||||
@@ -267,6 +267,14 @@ describeEmbeddedPostgres("routine routes end-to-end", () => {
|
||||
expect(runRes.body.source).toBe("manual");
|
||||
expect(runRes.body.linkedIssueId).toBeTruthy();
|
||||
|
||||
const listRes = await request(app).get(`/api/companies/${companyId}/routines`);
|
||||
expect(listRes.status).toBe(200);
|
||||
const listed = listRes.body.find((r: { id: string }) => r.id === routineId);
|
||||
expect(listed).toBeDefined();
|
||||
expect(listed.triggers).toHaveLength(1);
|
||||
expect(listed.triggers[0].cronExpression).toBe("0 10 * * 1-5");
|
||||
expect(listed.triggers[0].timezone).toBe("UTC");
|
||||
|
||||
const detailRes = await request(app).get(`/api/routines/${routineId}`);
|
||||
expect(detailRes.status).toBe(200);
|
||||
expect(detailRes.body.triggers).toHaveLength(1);
|
||||
|
||||
@@ -921,6 +921,8 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
|
||||
kind: trigger.kind as RoutineListItem["triggers"][number]["kind"],
|
||||
label: trigger.label,
|
||||
enabled: trigger.enabled,
|
||||
cronExpression: trigger.cronExpression,
|
||||
timezone: trigger.timezone,
|
||||
nextRunAt: trigger.nextRunAt,
|
||||
lastFiredAt: trigger.lastFiredAt,
|
||||
lastResult: trigger.lastResult,
|
||||
|
||||
Reference in New Issue
Block a user