Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e19d5d
DELAY_ACTIONS impl plan
edulelis Jun 18, 2025
32b2cfb
Delayed actions initial implementation
edulelis Jun 20, 2025
d2c5ca3
Merge branch 'digest-emails' of github.com:edulelis/inbox-zero into d…
edulelis Jun 20, 2025
728435a
Merge branch 'main' of github.com:elie222/inbox-zero into delayed-act…
edulelis Jun 23, 2025
d0a2c93
Improve tests. PR cleanup + PR feedback. Fix bugs.
edulelis Jun 24, 2025
610a9cc
Merge branch 'main' of github.com:elie222/inbox-zero into delayed-act…
edulelis Jun 24, 2025
0a87d54
Refactoring. Improve UI elements. Add admin endpoint. Clean up functi…
edulelis Jun 25, 2025
217dad5
Merge branch 'main' of github.com:elie222/inbox-zero into delayed-act…
edulelis Jun 25, 2025
7d9811b
Clean up mdc file. Add migrations
edulelis Jun 25, 2025
4e9180d
Comments and variable names
edulelis Jun 25, 2025
300d336
PR feedback
edulelis Jun 25, 2025
827c424
Refactor to use QStash. UI fixes
edulelis Jun 26, 2025
11feeb4
PR feedback
edulelis Jun 26, 2025
375642d
Merge branch 'main' of github.com:elie222/inbox-zero into delayed-act…
edulelis Jun 26, 2025
66910a7
Fix delay input/switch on empty rule action creation
edulelis Jun 26, 2025
6a57632
Fix broken test
edulelis Jun 27, 2025
2b9e207
PR feedback
edulelis Jun 30, 2025
71d6303
Bugfixes and PR feedback
edulelis Jul 9, 2025
6f1cc14
PR feedback
edulelis Jul 9, 2025
00881ba
PR feedback
edulelis Jul 10, 2025
a298f13
Merge branch 'main' of github.com:elie222/inbox-zero into delayed-act…
edulelis Jul 14, 2025
e614a9b
Remove onClick null delay
edulelis Jul 14, 2025
1dd91c9
Clean admin. Fix missing schema field
edulelis Jul 14, 2025
922e979
PR feedback on RuleForm
edulelis Jul 15, 2025
b762db6
PR feedback
edulelis Jul 16, 2025
facf647
PR feedback
edulelis Jul 16, 2025
8cbee38
PR feedback
edulelis Jul 17, 2025
e308f44
Cleanup comments
edulelis Jul 17, 2025
ffd6d6e
Feature flags
edulelis Jul 18, 2025
e3fef98
Merge branch 'main' of github.com:elie222/inbox-zero into delayed-act…
edulelis Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions .cursor/rules/features/delayed-actions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Delayed Actions Feature

## Overview

The delayed actions feature allows users to schedule email actions (like labeling, archiving, or replying) to be executed after a specified delay period. This is useful for scenarios like:

- **Follow-up reminders**: Label emails that haven't been replied to after X days
- **Snooze functionality**: Archive emails and bring them back later
- **Time-sensitive processing**: Apply actions only after a waiting period

## Implementation Architecture

### Core Components

1. **Action Delay Configuration**
- `Action.delayInMinutes` field: Optional delay from 1 minute to 90 days
- UI controls in `RuleForm.tsx` for setting delays
- Validation ensures delays are within acceptable bounds

2. **Scheduled Action Storage**
- `ScheduledAction` model: Stores pending delayed actions
- Contains action details, timing, and execution status
- Links to `ExecutedRule` for context and audit trail

3. **QStash Integration**
- Uses Upstash QStash for reliable message queuing
- Replaces cron-based polling with event-driven execution
- Provides built-in retries and error handling

### Database Schema

```prisma
model ScheduledAction {
id String @id @default(cuid())
executedRuleId String
actionType ActionType
messageId String
threadId String
scheduledFor DateTime
emailAccountId String
status ScheduledActionStatus @default(PENDING)

// Action-specific fields
label String?
subject String?
content String?
to String?
cc String?
bcc String?
url String?

// QStash integration
scheduledId String?

// Execution tracking
executedAt DateTime?
executedActionId String? @unique

// Relationships and indexes...
}
```

## QStash Integration

### Scheduling Process

1. **Rule Execution**: When a rule matches an email, actions are split into:
- **Immediate actions**: Executed right away
- **Delayed actions**: Scheduled via QStash

2. **QStash Scheduling**:
```typescript
const notBefore = getUnixTime(addMinutes(new Date(), delayInMinutes));

const response = await qstash.publishJSON({
url: `${process.env.NEXTAUTH_URL}/api/scheduled-actions/execute`,
body: {
scheduledActionId: scheduledAction.id,
},
notBefore, // Unix timestamp for when to execute
deduplicationId: `scheduled-action-${scheduledAction.id}`,
});
```

3. **Deduplication**: Uses unique IDs to prevent duplicate execution
4. **Message ID Storage**: QStash scheduledId stored for efficient cancellation (field: scheduledId)

### Execution Process

1. **QStash Delivery**: QStash delivers message to `/api/scheduled-actions/execute`
2. **Signature Verification**: Validates QStash signature for security
3. **Action Execution**:
- Retrieves scheduled action from database
- Validates email still exists
- Executes the specific action using `runActionFunction`
- Updates execution status

### Benefits Over Cron-Based Approach

- **Reliability**: No polling, exact scheduling, built-in retries
- **Scalability**: No background processes, QStash handles infrastructure
- **Deduplication**: Prevents duplicate execution with unique IDs
- **Monitoring**: Better observability through QStash dashboard
- **Cancellation**: Direct message cancellation using stored message IDs

## Key Functions

### Core Scheduling Functions

```typescript
// Create and schedule a single delayed action
export async function createScheduledAction({
executedRuleId,
actionItem,
messageId,
threadId,
emailAccountId,
scheduledFor,
})

// Schedule multiple delayed actions for a rule execution
export async function scheduleDelayedActions({
executedRuleId,
actionItems,
messageId,
threadId,
emailAccountId,
})

// Cancel existing scheduled actions (e.g., when new rule overrides)
export async function cancelScheduledActions({
emailAccountId,
messageId,
threadId,
reason,
})
```

### Usage in Rule Execution

```typescript
// In run-rules.ts
// Cancel any existing scheduled actions for this message
await cancelScheduledActions({
emailAccountId: emailAccount.id,
messageId: message.id,
threadId: message.threadId,
reason: "Superseded by new rule execution",
});

// Schedule delayed actions if any exist
if (executedRule && delayedActions.length > 0 && !isTest) {
await scheduleDelayedActions({
executedRuleId: executedRule.id,
actionItems: delayedActions,
messageId: message.id,
threadId: message.threadId,
emailAccountId: emailAccount.id,
});
}
```

## Migration Safety

The database migration includes `IF NOT EXISTS` clauses to prevent conflicts:

```sql
-- CreateEnum
CREATE TYPE IF NOT EXISTS "ScheduledActionStatus" AS ENUM ('PENDING', 'EXECUTING', 'COMPLETED', 'FAILED', 'CANCELLED');

-- AlterTable
ALTER TABLE "Action" ADD COLUMN IF NOT EXISTS "delayInMinutes" INTEGER;

-- CreateTable
CREATE TABLE IF NOT EXISTS "ScheduledAction" (
-- table definition
);

-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "ScheduledAction_executedActionId_key" ON "ScheduledAction"("executedActionId");
```

## Usage Examples

### Basic Delay Configuration
```typescript
// In rule action configuration
{
type: "LABEL",
label: "Follow-up Needed",
delayInMinutes: 2880 // 2 days
}
```

### Follow-up Workflow
1. Email arrives and matches rule
2. Immediate action: Archive email
3. Delayed action: Label as "Follow-up" after 3 days
4. If user replies before 3 days, action can be cancelled

## API Endpoints

- `POST /api/scheduled-actions/execute`: QStash webhook for execution
- `DELETE /api/admin/scheduled-actions/[id]/cancel`: Cancel scheduled action
- `POST /api/admin/scheduled-actions/[id]/retry`: Retry failed action

## Error Handling

- **Email Not Found**: Action marked as completed with reason
- **Execution Failure**: Action marked as failed, logged for debugging
- **Cancellation**: QStash message cancelled, database updated
- **Retries**: QStash automatically retries failed deliveries

## Monitoring

- Database status tracking: PENDING → EXECUTING → COMPLETED/FAILED
- QStash dashboard for message delivery monitoring
- Structured logging for debugging and observability
1 change: 1 addition & 0 deletions apps/web/__tests__/ai-choose-rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ describe.runIf(isAiTest)("aiChooseRule", () => {
cc: null,
bcc: null,
url: null,
delayInMinutes: null,
},
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function useDraftReplies() {
cc: null,
bcc: null,
url: null,
delayInMinutes: null,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/(app)/[emailAccountId]/assistant/RuleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function RuleDialog({
initialRule,
editMode = true,
}: RuleDialogProps) {
const { data, isLoading, error } = useRule(ruleId || "");
const { data, isLoading, error, mutate } = useRule(ruleId || "");

const handleSuccess = () => {
onSuccess?.();
Expand All @@ -52,6 +52,7 @@ export function RuleDialog({
alwaysEditMode={editMode}
onSuccess={handleSuccess}
isDialog={true}
mutate={mutate}
/>
)}
</LoadingContent>
Expand Down
Loading
Loading