Skip to content

Commit 2673cc9

Browse files
arpandhakalclaude
andcommitted
fix(templates): keep the parent task when a subtask fails to apply
createSubtasksFromTemplate rolled back on failure by soft-deleting `parentId` — which is the *parent* (main) task, not the failed subtask. So any single subtask-creation error (e.g. the connection-pool P2024) silently deleted the task the user had just created and verified, and Realtime made it vanish from their screen ("task disappeared on its own" in OUT-3964). The rollback was also wrong (it orphaned already-created sibling subtasks) and unnecessary — createTask already cleans up a half-created subtask via its own catch. Now each subtemplate is applied best-effort: the whole per-subtemplate unit (getAppliedTemplateDescription + create) runs inside one try/catch that skips and logs on failure instead of throwing. Because the handler no longer rejects, runInBatches never aborts, so every subtemplate is attempted and one failure no longer skips later batches or returns an error after a partial write (fixes the two P1 review findings). The parent task is never deleted or orphaned. Refactored createSubtasksFromTemplate to object params and folded the applied- description fetch inside it, so both the internal and public call sites share the same failure isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 05dea0e commit 2673cc9

3 files changed

Lines changed: 36 additions & 24 deletions

File tree

‎src/app/api/tasks/public/public.service.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,13 @@ export class PublicTasksService extends TasksSharedService {
273273

274274
if (template.subTaskTemplates.length) {
275275
await runInBatches(template.subTaskTemplates, subtaskTemplateBatchSize, async (sub, index) => {
276-
const updatedSubTemplate = await templateService.getAppliedTemplateDescription(sub.id)
277-
const manualTimeStamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
278-
await this.createSubtasksFromTemplate(updatedSubTemplate, newTask, manualTimeStamp)
276+
const manualTimestamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
277+
await this.createSubtasksFromTemplate({
278+
subTemplateId: sub.id,
279+
parentTask: newTask,
280+
manualTimestamp,
281+
templateService,
282+
})
279283
})
280284
}
281285
}

‎src/app/api/tasks/tasks.service.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,13 @@ export class TasksService extends TasksSharedService {
254254

255255
if (template.subTaskTemplates.length) {
256256
await runInBatches(template.subTaskTemplates, subtaskTemplateBatchSize, async (sub, index) => {
257-
const updatedSubTemplate = await templateService.getAppliedTemplateDescription(sub.id)
258-
const manualTimeStamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
259-
await this.createSubtasksFromTemplate(updatedSubTemplate, newTask, manualTimeStamp)
257+
const manualTimestamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
258+
await this.createSubtasksFromTemplate({
259+
subTemplateId: sub.id,
260+
parentTask: newTask,
261+
manualTimestamp,
262+
templateService,
263+
})
260264
})
261265
}
262266
}

‎src/app/api/tasks/tasksShared.service.ts‎

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import { SupabaseActions } from '@/utils/SupabaseActions'
1818
import APIError from '@api/core/exceptions/api'
1919
import { BaseService } from '@api/core/services/base.service'
2020
import { UserRole } from '@api/core/types/user'
21-
import { AssigneeType, Prisma, PrismaClient, StateType, Task, TaskTemplate } from '@prisma/client'
21+
import { AssigneeType, Prisma, StateType, Task } from '@prisma/client'
2222
import httpStatus from 'http-status'
2323
import z from 'zod'
2424
import { AttachmentsService } from '@api/attachments/attachments.service'
25+
import type { TemplatesService } from '@api/tasks/templates/templates.service'
2526

2627
//Base class with shared permission logic and methods that both tasks.service.ts and public.service.ts could use
2728
export abstract class TasksSharedService extends BaseService {
@@ -589,11 +590,22 @@ export abstract class TasksSharedService extends BaseService {
589590
}
590591
}
591592

592-
protected async createSubtasksFromTemplate(data: TaskTemplate, parentTask: Task, manualTimestamp: Date) {
593-
const { workspaceId, title, body, workflowStateId } = data
593+
protected async createSubtasksFromTemplate({
594+
subTemplateId,
595+
parentTask,
596+
manualTimestamp,
597+
templateService,
598+
}: {
599+
subTemplateId: string
600+
parentTask: Task
601+
manualTimestamp: Date
602+
templateService: TemplatesService
603+
}) {
594604
const { id: parentId, internalUserId, clientId, companyId, associations, isShared } = parentTask
595605

596606
try {
607+
const { workspaceId, title, body, workflowStateId } =
608+
await templateService.getAppliedTemplateDescription(subTemplateId)
597609
const createTaskPayload = CreateTaskRequestSchema.parse({
598610
title: resolveDynamicFields(title),
599611
body: body ? resolveAutofillTags(body) : body,
@@ -608,23 +620,15 @@ export abstract class TasksSharedService extends BaseService {
608620
isShared,
609621
})
610622

611-
await this.createTask(createTaskPayload, { disableSubtaskTemplates: true, manualTimestamp: manualTimestamp })
623+
await this.createTask(createTaskPayload, { disableSubtaskTemplates: true, manualTimestamp })
612624
} catch (e) {
613-
const deleteTask = this.db.task.delete({ where: { id: parentId } })
614-
const deleteActivityLogs = this.db.activityLog.deleteMany({ where: { taskId: parentId } })
615-
616-
await this.db.$transaction(async (tx) => {
617-
this.setTransaction(tx as PrismaClient)
618-
await deleteTask
619-
await deleteActivityLogs
620-
this.unsetTransaction()
625+
// A failed subtask must never delete or abort the already-created parent task
626+
// (previously caused templated tasks to vanish). Skip it; the rest still get created.
627+
console.error('TasksSharedService#createSubtasksFromTemplate | Skipping subtask that failed to create', {
628+
parentId,
629+
subTemplateId,
630+
e,
621631
})
622-
623-
console.error('TasksService#createTask | Rolling back task creation', e)
624-
throw new APIError(
625-
httpStatus.INTERNAL_SERVER_ERROR,
626-
'Failed to create subtask from template, new task was not created.',
627-
)
628632
}
629633
}
630634

0 commit comments

Comments
 (0)