Skip to content

Commit 153cb09

Browse files
committed
feat(schedules): remove save button for schedules, couple schedule deployment with workflow deployment
1 parent eaca490 commit 153cb09

File tree

27 files changed

+1516
-1875
lines changed

27 files changed

+1516
-1875
lines changed

apps/docs/content/docs/en/triggers/schedule.mdx

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ title: Schedule
55
import { Callout } from 'fumadocs-ui/components/callout'
66
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
77
import { Image } from '@/components/ui/image'
8-
import { Video } from '@/components/ui/video'
98

109
The Schedule block automatically triggers workflows on a recurring schedule at specified intervals or times.
1110

@@ -21,16 +20,16 @@ The Schedule block automatically triggers workflows on a recurring schedule at s
2120

2221
## Schedule Options
2322

24-
Configure when your workflow runs using the dropdown options:
23+
Configure when your workflow runs:
2524

2625
<Tabs items={['Simple Intervals', 'Cron Expressions']}>
2726
<Tab>
2827
<ul className="list-disc space-y-1 pl-6">
29-
<li><strong>Every few minutes</strong>: 5, 15, 30 minute intervals</li>
30-
<li><strong>Hourly</strong>: Every hour or every few hours</li>
31-
<li><strong>Daily</strong>: Once or multiple times per day</li>
32-
<li><strong>Weekly</strong>: Specific days of the week</li>
33-
<li><strong>Monthly</strong>: Specific days of the month</li>
28+
<li><strong>Every X Minutes</strong>: Run at minute intervals (1-1440)</li>
29+
<li><strong>Hourly</strong>: Run at a specific minute each hour</li>
30+
<li><strong>Daily</strong>: Run at a specific time each day</li>
31+
<li><strong>Weekly</strong>: Run on a specific day and time each week</li>
32+
<li><strong>Monthly</strong>: Run on a specific day and time each month</li>
3433
</ul>
3534
</Tab>
3635
<Tab>
@@ -43,24 +42,25 @@ Configure when your workflow runs using the dropdown options:
4342
</Tab>
4443
</Tabs>
4544

46-
## Configuring Schedules
45+
## Activation
4746

48-
When a workflow is scheduled:
49-
- The schedule becomes **active** and shows the next execution time
50-
- Click the **"Scheduled"** button to deactivate the schedule
51-
- Schedules automatically deactivate after **3 consecutive failures**
47+
Schedules are tied to workflow deployment:
5248

53-
<div className="flex justify-center">
54-
<Image
55-
src="/static/blocks/schedule-2.png"
56-
alt="Active Schedule Block"
57-
width={500}
58-
height={400}
59-
className="my-6"
60-
/>
61-
</div>
49+
- **Deploy workflow** → Schedule becomes active and starts running
50+
- **Undeploy workflow** → Schedule is removed
51+
- **Redeploy workflow** → Schedule is recreated with current configuration
52+
53+
<Callout>
54+
You must deploy your workflow for the schedule to start running. Configure the schedule block, then deploy from the toolbar.
55+
</Callout>
6256

63-
## Disabled Schedules
57+
## Automatic Disabling
58+
59+
Schedules automatically disable after **10 consecutive failures** to prevent runaway errors. When disabled:
60+
61+
- A warning badge appears on the schedule block
62+
- The schedule stops executing
63+
- Click the badge to reactivate the schedule
6464

6565
<div className="flex justify-center">
6666
<Image
@@ -72,8 +72,6 @@ When a workflow is scheduled:
7272
/>
7373
</div>
7474

75-
Disabled schedules show when they were last active. Click the **"Disabled"** badge to reactivate the schedule.
76-
7775
<Callout>
78-
Schedule blocks cannot receive incoming connections and serve as pure workflow triggers.
79-
</Callout>
76+
Schedule blocks cannot receive incoming connections and serve as workflow entry points only.
77+
</Callout>

apps/sim/app/api/schedules/[id]/route.ts

Lines changed: 24 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -12,98 +12,19 @@ const logger = createLogger('ScheduleAPI')
1212

1313
export const dynamic = 'force-dynamic'
1414

15-
const scheduleActionEnum = z.enum(['reactivate', 'disable'])
16-
const scheduleStatusEnum = z.enum(['active', 'disabled'])
17-
18-
const scheduleUpdateSchema = z
19-
.object({
20-
action: scheduleActionEnum.optional(),
21-
status: scheduleStatusEnum.optional(),
22-
})
23-
.refine((data) => data.action || data.status, {
24-
message: 'Either action or status must be provided',
25-
})
15+
const scheduleUpdateSchema = z.object({
16+
action: z.literal('reactivate'),
17+
})
2618

2719
/**
28-
* Delete a schedule
29-
*/
30-
export async function DELETE(
31-
request: NextRequest,
32-
{ params }: { params: Promise<{ id: string }> }
33-
) {
34-
const requestId = generateRequestId()
35-
36-
try {
37-
const { id } = await params
38-
logger.debug(`[${requestId}] Deleting schedule with ID: ${id}`)
39-
40-
const session = await getSession()
41-
if (!session?.user?.id) {
42-
logger.warn(`[${requestId}] Unauthorized schedule deletion attempt`)
43-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
44-
}
45-
46-
// Find the schedule and check ownership
47-
const schedules = await db
48-
.select({
49-
schedule: workflowSchedule,
50-
workflow: {
51-
id: workflow.id,
52-
userId: workflow.userId,
53-
workspaceId: workflow.workspaceId,
54-
},
55-
})
56-
.from(workflowSchedule)
57-
.innerJoin(workflow, eq(workflowSchedule.workflowId, workflow.id))
58-
.where(eq(workflowSchedule.id, id))
59-
.limit(1)
60-
61-
if (schedules.length === 0) {
62-
logger.warn(`[${requestId}] Schedule not found: ${id}`)
63-
return NextResponse.json({ error: 'Schedule not found' }, { status: 404 })
64-
}
65-
66-
const workflowRecord = schedules[0].workflow
67-
68-
// Check authorization - either the user owns the workflow or has write/admin workspace permissions
69-
let isAuthorized = workflowRecord.userId === session.user.id
70-
71-
// If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions
72-
if (!isAuthorized && workflowRecord.workspaceId) {
73-
const userPermission = await getUserEntityPermissions(
74-
session.user.id,
75-
'workspace',
76-
workflowRecord.workspaceId
77-
)
78-
isAuthorized = userPermission === 'write' || userPermission === 'admin'
79-
}
80-
81-
if (!isAuthorized) {
82-
logger.warn(`[${requestId}] Unauthorized schedule deletion attempt for schedule: ${id}`)
83-
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
84-
}
85-
86-
// Delete the schedule
87-
await db.delete(workflowSchedule).where(eq(workflowSchedule.id, id))
88-
89-
logger.info(`[${requestId}] Successfully deleted schedule: ${id}`)
90-
return NextResponse.json({ success: true }, { status: 200 })
91-
} catch (error) {
92-
logger.error(`[${requestId}] Error deleting schedule`, error)
93-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
94-
}
95-
}
96-
97-
/**
98-
* Update a schedule - can be used to reactivate a disabled schedule
20+
* Reactivate a disabled schedule
9921
*/
10022
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
10123
const requestId = generateRequestId()
10224

10325
try {
104-
const { id } = await params
105-
const scheduleId = id
106-
logger.debug(`[${requestId}] Updating schedule with ID: ${scheduleId}`)
26+
const { id: scheduleId } = await params
27+
logger.debug(`[${requestId}] Reactivating schedule with ID: ${scheduleId}`)
10728

10829
const session = await getSession()
10930
if (!session?.user?.id) {
@@ -115,13 +36,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
11536
const validation = scheduleUpdateSchema.safeParse(body)
11637

11738
if (!validation.success) {
118-
const firstError = validation.error.errors[0]
119-
logger.warn(`[${requestId}] Validation error:`, firstError)
120-
return NextResponse.json({ error: firstError.message }, { status: 400 })
39+
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
12140
}
12241

123-
const { action, status: requestedStatus } = validation.data
124-
12542
const [schedule] = await db
12643
.select({
12744
id: workflowSchedule.id,
@@ -164,57 +81,29 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
16481
return NextResponse.json({ error: 'Not authorized to modify this schedule' }, { status: 403 })
16582
}
16683

167-
if (action === 'reactivate' || (requestedStatus && requestedStatus === 'active')) {
168-
if (schedule.status === 'active') {
169-
return NextResponse.json({ message: 'Schedule is already active' }, { status: 200 })
170-
}
171-
172-
const now = new Date()
173-
const nextRunAt = new Date(now.getTime() + 60 * 1000) // Schedule to run in 1 minute
174-
175-
await db
176-
.update(workflowSchedule)
177-
.set({
178-
status: 'active',
179-
failedCount: 0,
180-
updatedAt: now,
181-
nextRunAt,
182-
})
183-
.where(eq(workflowSchedule.id, scheduleId))
84+
if (schedule.status === 'active') {
85+
return NextResponse.json({ message: 'Schedule is already active' }, { status: 200 })
86+
}
18487

185-
logger.info(`[${requestId}] Reactivated schedule: ${scheduleId}`)
88+
const now = new Date()
89+
const nextRunAt = new Date(now.getTime() + 60 * 1000) // Schedule to run in 1 minute
18690

187-
return NextResponse.json({
188-
message: 'Schedule activated successfully',
91+
await db
92+
.update(workflowSchedule)
93+
.set({
94+
status: 'active',
95+
failedCount: 0,
96+
updatedAt: now,
18997
nextRunAt,
19098
})
191-
}
192-
193-
if (action === 'disable' || (requestedStatus && requestedStatus === 'disabled')) {
194-
if (schedule.status === 'disabled') {
195-
return NextResponse.json({ message: 'Schedule is already disabled' }, { status: 200 })
196-
}
197-
198-
const now = new Date()
199-
200-
await db
201-
.update(workflowSchedule)
202-
.set({
203-
status: 'disabled',
204-
updatedAt: now,
205-
nextRunAt: null, // Clear next run time when disabled
206-
})
207-
.where(eq(workflowSchedule.id, scheduleId))
208-
209-
logger.info(`[${requestId}] Disabled schedule: ${scheduleId}`)
99+
.where(eq(workflowSchedule.id, scheduleId))
210100

211-
return NextResponse.json({
212-
message: 'Schedule disabled successfully',
213-
})
214-
}
101+
logger.info(`[${requestId}] Reactivated schedule: ${scheduleId}`)
215102

216-
logger.warn(`[${requestId}] Unsupported update action for schedule: ${scheduleId}`)
217-
return NextResponse.json({ error: 'Unsupported update action' }, { status: 400 })
103+
return NextResponse.json({
104+
message: 'Schedule activated successfully',
105+
nextRunAt,
106+
})
218107
} catch (error) {
219108
logger.error(`[${requestId}] Error updating schedule`, error)
220109
return NextResponse.json({ error: 'Failed to update schedule' }, { status: 500 })

0 commit comments

Comments
 (0)