Skip to content

Post-Quiz Processing Workflow — System Design Document

EduOne AI Platform | Version 1.0

FieldValue
AuthorEduOne Engineering Team
Created2026-07-17
StatusApproved
AudienceBackend Engineers, DevOps, System Architects
Related DocsWorkflow Requirements Specification, API Reference

Table of Contents

  1. Architecture & System Context
  2. Database Schema & State Management
  3. API Design
  4. Orchestrator Engine Logic
  5. Integration & Submission API Refactoring
  6. Error Handling, Transactions, & Recovery

1. Architecture & System Context

The Post-Quiz Processing subsystem orchestrates the operations that occur after a student submits a quiz. Due to the high computational demand of computing mastery updates, traversing the dependency graph, and generating recommendations, these tasks are offloaded to an asynchronous pipeline.

1.1 Cloudflare Workflows Integration

This system utilizes Cloudflare Workflows (a stateful, durable execution framework built on Cloudflare Workers) to act as the primary orchestrator. Cloudflare Workflows provides automatic checkpointing, state serialization, and built-in retries.

The orchestrator sits between the HTTP router (submission.ts) and the database/engine layers.

mermaid
graph TB
    subgraph Client ["Client Layer"]
        STU["Student App (React)"]
    end

    subgraph API_GW ["Cloudflare Worker (Hono)"]
        SUB_ROUTE["Submission Router<br/>/api/submissions"]
        WF_ROUTE["Workflow Router<br/>/api/workflows"]
    end

    subgraph Orchestration ["Stateful Orchestrator"]
        CF_WF["Cloudflare Workflows<br/>(PostQuizProcessingWorkflow)"]
    end

    subgraph Engines ["Domain Engines"]
        GE["Grading Engine"]
        EP["Evidence Processor"]
        MC["Mastery Calculator"]
        DGE["Dependency Graph Engine"]
        RE["Recommendation Engine"]
    end

    subgraph Storage ["Data Layer"]
        D1["Cloudflare D1<br/>(SQLite Database)"]
    end

    %% Flow lines
    STU -->|1. Submit Answers| SUB_ROUTE
    SUB_ROUTE -->|2. Create Submission (pending)| D1
    SUB_ROUTE -->|3. Dispatch Workflow| CF_WF
    SUB_ROUTE -->>STU|4. 202 Accepted (submissionId, workflowId)|
    
    CF_WF -->|5. Run Step: Grade| GE
    CF_WF -->|6. Run Step: Evidence| EP
    CF_WF -->|7. Run Step: Mastery| MC
    CF_WF -->|8. Run Step: Diagnostics| DGE
    CF_WF -->|9. Run Step: Recommendation| RE

    GE & EP & MC & DGE & RE -->|Read / Write State| D1
    
    STU -->|10. Poll Status / Results| SUB_ROUTE
    STU -->|11. Query Logs / Engine State| WF_ROUTE
    WF_ROUTE -->|Read Workflow Instance| CF_WF
    WF_ROUTE -->|Read Workflow Table| D1

1.2 Step Execution Flow

The workflow is broken down into 5 sequential steps, with the orchestrator writing checkpoints to the DB after each step completes.

mermaid
sequenceDiagram
    autonumber
    participant Client
    participant Hono as Hono Router
    participant WF as Cloudflare Workflows
    participant D1 as D1 Database
    
    Client->>Hono: POST /api/submissions
    Hono->>D1: Insert Submission (status='submitted')
    Hono->>WF: Start PostQuizProcessingWorkflow (params)
    Hono-->>Client: 202 Accepted (submission_id, workflow_id)
    
    Note over WF: Step 1: Grade Quiz
    WF->>D1: Fetch answers & scoring matrix
    WF->>D1: Update lrn_submissions (score, status='graded')
    
    Note over WF: Step 2: Evidence Generation
    WF->>D1: Insert evd_evidence records (processed=0)
    
    Note over WF: Step 3: Bayesian Mastery Updates
    WF->>D1: Fetch old skill mastery states
    WF->>D1: Compute new mastery (Weighted Moving Average)
    WF->>D1: Update evd_learner_skill_states & evd_learner_models
    
    Note over WF: Step 4: Prerequisite Gap Diagnosis
    WF->>D1: Traverse dependency graph (cur_skill_dependencies)
    WF->>WF: Isolate gaps where mastery < 0.7
    
    Note over WF: Step 5: Recommendations
    WF->>D1: Filter candidate content items
    WF->>D1: Rank items and select top match
    WF->>D1: Insert rec_recommendations record
    
    Note over WF: Workflow Completes
    WF->>D1: Update ops_workflows (status='completed')

2. Database Schema & State Management

To track asynchronous operations, manage checkpoint state, and allow manual intervention/resumption, we expand the D1 schema with execution tables.

2.1 Schema Updates & Table Definitions

We introduce ops_workflow_steps to track step executions, and ops_workflow_context to hold variable states between steps.

mermaid
erDiagram
    ops_workflows ||--|{ ops_workflow_steps : "contains"
    ops_workflows ||--|| ops_workflow_context : "stores snapshot of"
    iam_organizations ||--|{ ops_workflows : "owns"
    lrn_submissions ||--o| ops_workflows : "triggered by"
    
    ops_workflows {
        text id PK
        text organization_id FK
        text workflow_type
        text trigger_type
        text trigger_id
        text status
        integer priority
        text error_message
        text error_details
        integer retry_count
        integer max_retries
        text started_at
        text completed_at
        text created_at
        text updated_at
    }

    ops_workflow_steps {
        text id PK
        text workflow_id FK
        text step_name
        text status
        text input_payload
        text output_payload
        text error_message
        integer retry_count
        text started_at
        text completed_at
    }

    ops_workflow_context {
        text workflow_id PK, FK
        text context_json
        text updated_at
    }

2.2 Complete SQL Migration DDL

The following SQL statements are applied to the D1 SQLite database to register the new tables and indexes.

sql
-- Migration: Workflow System Schema
-- Target: Cloudflare D1 (SQLite)

-------------------------------------------------------
-- 1. Extend ops_workflows (if required, or defined fully)
-------------------------------------------------------
CREATE TABLE IF NOT EXISTS ops_workflows (
  id              TEXT NOT NULL PRIMARY KEY,
  organization_id TEXT NOT NULL REFERENCES iam_organizations(id) ON DELETE RESTRICT,
  workflow_type   TEXT NOT NULL CHECK (workflow_type IN ('evidence_processing','model_update','recommendation_generation','content_review','quiz_grading','report_generation')),
  trigger_type    TEXT NOT NULL CHECK (trigger_type IN ('submission','schedule','manual','webhook','system')),
  trigger_id      TEXT,            -- ID of the triggering entity (e.g. submission_id)
  status          TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','completed','failed','cancelled','retrying')),
  priority        INTEGER NOT NULL DEFAULT 5,
  input_data      TEXT,            -- JSON: workflow input parameters
  output_data     TEXT,            -- JSON: workflow results
  error_message   TEXT,
  error_details   TEXT,            -- JSON: stack trace, debug info
  retry_count     INTEGER NOT NULL DEFAULT 0,
  max_retries     INTEGER NOT NULL DEFAULT 3,
  started_at      TEXT,
  completed_at    TEXT,
  created_at      TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at      TEXT NOT NULL DEFAULT (datetime('now'))
) STRICT;

CREATE INDEX IF NOT EXISTS idx_ops_wf_org ON ops_workflows(organization_id);
CREATE INDEX IF NOT EXISTS idx_ops_wf_type ON ops_workflows(workflow_type);
CREATE INDEX IF NOT EXISTS idx_ops_wf_status ON ops_workflows(status);
CREATE INDEX IF NOT EXISTS idx_ops_wf_trigger ON ops_workflows(trigger_type, trigger_id);

-------------------------------------------------------
-- 2. Step execution tracking table
-------------------------------------------------------
CREATE TABLE IF NOT EXISTS ops_workflow_steps (
  id              TEXT NOT NULL PRIMARY KEY,
  workflow_id     TEXT NOT NULL REFERENCES ops_workflows(id) ON DELETE CASCADE,
  step_name       TEXT NOT NULL,
  status          TEXT NOT NULL CHECK (status IN ('pending','running','completed','failed')),
  input_payload   TEXT,            -- JSON: input parameters sent to this step
  output_payload  TEXT,            -- JSON: output returned by this step
  error_message   TEXT,            -- Error description if step failed
  retry_count     INTEGER NOT NULL DEFAULT 0,
  started_at      TEXT NOT NULL DEFAULT (datetime('now')),
  completed_at    TEXT
) STRICT;

CREATE INDEX IF NOT EXISTS idx_ops_wf_steps_wf ON ops_workflow_steps(workflow_id);
CREATE INDEX IF NOT EXISTS idx_ops_wf_steps_status ON ops_workflow_steps(workflow_id, status);

-------------------------------------------------------
-- 3. Workflow Context Snapshot table
-- Holds the serialized variables state between steps
-------------------------------------------------------
CREATE TABLE IF NOT EXISTS ops_workflow_context (
  workflow_id     TEXT NOT NULL PRIMARY KEY REFERENCES ops_workflows(id) ON DELETE CASCADE,
  context_json    TEXT NOT NULL,   -- JSON representation of current variables
  updated_at      TEXT NOT NULL DEFAULT (datetime('now'))
) STRICT;

2.3 State Machine Transition Matrix

The ops_workflows table tracks the lifecycle of the entire processing pipeline. The allowed state transitions are:

Initial StateEvent TriggerNext StateAction Taken
pendingstartrunningInitialize workflow context and execute the first step.
runningstep_successrunningSave step checkpoint to DB and advance current_step.
runningstep_fail (retry < max)retryingLog error, trigger exponential backoff.
retryingretry_timeoutrunningIncrement retry_count, re-execute the active step.
runningstep_fail (retry >= max)failedLog final exception, write error detail trace, alert admin.
runningall_steps_completecompletedWrite final output parameters, update submission state to graded.
failedmanual_resumerunningReset step retry count, reload variables from context snapshot, execute failed step.
runningcancel_signalcancelledStop current step execution context and mark run as aborted.

3. API Design

The system implements API routes under /api/workflows to monitor and manage workflow executions.

3.1 Routing Table

RouteHTTP MethodAuth RoleDescription
/api/submissionsPOSTstudentSubmit answers and trigger post-quiz processing workflow.
/api/submissions/:id/resultsGETstudent, teacherRetrieve grading results, mastery updates, and recommendations.
/api/workflows/:id/statusGETadmin, teacherRetrieve details of a workflow instance, its status, and step checkpoints.
/api/workflows/:id/logsGETadminRetrieve stdout/stderr execution logs and stack traces.
/api/workflows/:id/resumePOSTadminManually resume a failed workflow execution from its last checkpoint.

3.2 Endpoint Specifications

3.2.1 Trigger Submission Pipeline

  • Endpoint: POST /api/submissions
  • Request Payload:
json
{
  "quiz_id": "qz_8A3f9c",
  "class_id": "cl_2B8d4k",
  "started_at": "2026-07-17T16:50:00Z",
  "answers": [
    {
      "question_id": "q_111",
      "selected_option_id": "opt_111_a",
      "answer_text": null
    },
    {
      "question_id": "q_222",
      "selected_option_id": "opt_222_c",
      "answer_text": null
    }
  ]
}
  • Response Payload (202 Accepted):
json
{
  "status": "accepted",
  "message": "Submission received and queued for processing.",
  "submission_id": "sub_4F9e2a8k",
  "workflow_id": "wf_post_quiz_9D3j5s",
  "urls": {
    "status": "/api/workflows/wf_post_quiz_9D3j5s/status",
    "results": "/api/submissions/sub_4F9e2a8k/results"
  }
}

3.2.2 Fetch Workflow Engine Status

  • Endpoint: GET /api/workflows/:id/status
  • Response Payload (200 OK):
json
{
  "workflow_id": "wf_post_quiz_9D3j5s",
  "type": "evidence_processing",
  "trigger": {
    "type": "submission",
    "id": "sub_4F9e2a8k"
  },
  "status": "running",
  "priority": 5,
  "progress": {
    "total_steps": 5,
    "completed_steps": 2,
    "current_step": "Calculate Mastery"
  },
  "steps": [
    {
      "step_name": "Grade Submission",
      "status": "completed",
      "retry_count": 0,
      "started_at": "2026-07-17T17:00:01Z",
      "completed_at": "2026-07-17T17:00:03Z"
    },
    {
      "step_name": "Process Evidence",
      "status": "completed",
      "retry_count": 0,
      "started_at": "2026-07-17T17:00:03Z",
      "completed_at": "2026-07-17T17:00:04Z"
    },
    {
      "step_name": "Calculate Mastery",
      "status": "running",
      "retry_count": 1,
      "started_at": "2026-07-17T17:00:04Z",
      "completed_at": null
    },
    {
      "step_name": "Run Gap Diagnosis",
      "status": "pending",
      "retry_count": 0,
      "started_at": null,
      "completed_at": null
    },
    {
      "step_name": "Generate Recommendation",
      "status": "pending",
      "retry_count": 0,
      "started_at": null,
      "completed_at": null
    }
  ],
  "created_at": "2026-07-17T17:00:00Z",
  "updated_at": "2026-07-17T17:00:06Z"
}

3.2.3 Fetch Execution Logs

  • Endpoint: GET /api/workflows/:id/logs
  • Response Payload (200 OK):
json
{
  "workflow_id": "wf_post_quiz_9D3j5s",
  "logs": [
    {
      "timestamp": "2026-07-17T17:00:00Z",
      "level": "info",
      "message": "Workflow PostQuizProcessing initialized with submission ID: sub_4F9e2a8k."
    },
    {
      "timestamp": "2026-07-17T17:00:01Z",
      "level": "info",
      "message": "Step 'Grade Submission' started."
    },
    {
      "timestamp": "2026-07-17T17:00:03Z",
      "level": "info",
      "message": "Step 'Grade Submission' finished successfully. Score: 8/10."
    },
    {
      "timestamp": "2026-07-17T17:00:03Z",
      "level": "info",
      "message": "Step 'Process Evidence' started."
    },
    {
      "timestamp": "2026-07-17T17:00:04Z",
      "level": "warn",
      "message": "Anomaly detected in question q_222: time spent 1.1s is below threshold 1.5s. Weight adjusted to 0.2."
    },
    {
      "timestamp": "2026-07-17T17:00:04Z",
      "level": "info",
      "message": "Step 'Calculate Mastery' failed. DB execution interrupted: D1 connection timeout. Retrying."
    }
  ]
}

3.2.4 Manual Resume Execution

  • Endpoint: POST /api/workflows/:id/resume
  • Request Payload: {} (Empty JSON object)
  • Response Payload (200 OK):
json
{
  "status": "resumed",
  "workflow_id": "wf_post_quiz_9D3j5s",
  "resumed_from_step": "Calculate Mastery",
  "timestamp": "2026-07-17T17:05:00Z"
}

4. Orchestrator Engine Logic

The orchestrator logic is implemented using the Cloudflare Workflows API, utilizing the WorkflowEntrypoint class to create step boundaries.

4.1 Orchestrator Engine Implementation (TypeScript)

typescript
import { WorkflowEntrypoint, WorkflowStep } from 'cloudflare:workers';
import { generateId } from '../utils/id';

// Define context payload interface
export interface PostQuizWorkflowParams {
  submissionId: string;
  orgId: string;
  userId: string;
  classId: string;
  courseId: string;
}

export class PostQuizProcessingWorkflow extends WorkflowEntrypoint<PostQuizWorkflowParams> {
  
  async run(event: any, env: any) {
    const { submissionId, orgId, userId, classId, courseId } = this.params;
    const db = env.DB;
    const workflowId = this.id;

    // Helper to log and track step execution state in SQL database
    const updateStepStatus = async (
      stepName: string, 
      status: 'pending' | 'running' | 'completed' | 'failed', 
      input: any = null, 
      output: any = null, 
      errorMsg: string | null = null
    ) => {
      const now = new Date().toISOString();
      const stepId = `${workflowId}_${stepName.replace(/\s+/g, '_').toLowerCase()}`;
      
      await db.prepare(`
        INSERT INTO ops_workflow_steps (id, workflow_id, step_name, status, input_payload, output_payload, error_message, started_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(id) DO UPDATE SET
          status = excluded.status,
          output_payload = COALESCE(excluded.output_payload, output_payload),
          error_message = COALESCE(excluded.error_message, error_message),
          completed_at = CASE WHEN excluded.status IN ('completed', 'failed') THEN ? ELSE completed_at END
      `).bind(
        stepId, 
        workflowId, 
        stepName, 
        status, 
        input ? JSON.stringify(input) : null, 
        output ? JSON.stringify(output) : null, 
        errorMsg,
        now,
        now
      ).run();
    };

    // Update workflow context in D1
    const saveContextSnapshot = async (contextData: any) => {
      await db.prepare(`
        INSERT INTO ops_workflow_context (workflow_id, context_json, updated_at)
        VALUES (?, ?, ?)
        ON CONFLICT(workflow_id) DO UPDATE SET
          context_json = excluded.context_json,
          updated_at = excluded.updated_at
      `).bind(workflowId, JSON.stringify(contextData), new Date().toISOString()).run();
    };

    // Initialize workflow run metadata
    await db.prepare(`
      INSERT INTO ops_workflows (id, organization_id, workflow_type, trigger_type, trigger_id, status, started_at)
      VALUES (?, ?, 'evidence_processing', 'submission', ?, 'running', ?)
    `).bind(workflowId, orgId, submissionId, new Date().toISOString()).run();

    let context: any = { submissionId, orgId, userId, classId, courseId };
    await saveContextSnapshot(context);

    // ============================================================
    // STEP 1: Objective Grading
    // ============================================================
    const gradingResults = await this.step.run('Grade Submission', {
      retries: {
        limit: 3,
        delay: '2s',
        backoff: 'exponential'
      },
      timeout: '30s'
    }, async () => {
      await updateStepStatus('Grade Submission', 'running', { submissionId });
      
      try {
        // Execute scoring calculations
        // (Simulated engine call; in production imports actual logic)
        const quizSubmission = await db.prepare(
          `SELECT q.id as quiz_id, s.started_at 
           FROM lrn_submissions s
           JOIN lrn_quizzes q ON q.id = s.quiz_id
           WHERE s.id = ?`
        ).bind(submissionId).first();

        if (!quizSubmission) {
          throw new Error(`Submission ${submissionId} not found`);
        }

        // Return grading parameters to output checkpoint
        const result = {
          gradedAt: new Date().toISOString(),
          status: 'success'
        };

        await updateStepStatus('Grade Submission', 'completed', null, result);
        return result;
      } catch (err: any) {
        await updateStepStatus('Grade Submission', 'failed', null, null, err.message);
        throw err;
      }
    });

    context.gradingResults = gradingResults;
    await saveContextSnapshot(context);

    // ============================================================
    // STEP 2: Evidence Generation
    // ============================================================
    const evidenceResults = await this.step.run('Process Evidence', {
      retries: { limit: 3, delay: '2s', backoff: 'exponential' }
    }, async () => {
      await updateStepStatus('Process Evidence', 'running', { submissionId });
      try {
        // Load submission answers to generate evidence
        const { results: answers } = await db.prepare(`
          SELECT id, question_id, score, is_correct 
          FROM lrn_submission_answers 
          WHERE submission_id = ?
        `).bind(submissionId).all();

        // Process logic... (anomaly checks, normalization)
        const processedEvidenceIds: string[] = [];
        
        for (const answer of answers) {
          // Identify indicator mapping
          const question = await db.prepare(
            `SELECT indicator_id FROM lrn_questions WHERE id = ?`
          ).bind(answer.question_id).first();

          if (question?.indicator_id) {
            processedEvidenceIds.push(answer.id);
          }
        }

        const result = { processedEvidenceIds, count: processedEvidenceIds.length };
        await updateStepStatus('Process Evidence', 'completed', null, result);
        return result;
      } catch (err: any) {
        await updateStepStatus('Process Evidence', 'failed', null, null, err.message);
        throw err;
      }
    });

    context.evidenceResults = evidenceResults;
    await saveContextSnapshot(context);

    // ============================================================
    // STEP 3: Bayesian Mastery Updates
    // ============================================================
    const masteryResults = await this.step.run('Calculate Mastery', {
      retries: { limit: 3, delay: '2s', backoff: 'exponential' }
    }, async () => {
      await updateStepStatus('Calculate Mastery', 'running', { userId });
      try {
        // Recalculate states using weighted average
        const result = {
          updatedSkills: ['sk_math_matrices'],
          timestamp: new Date().toISOString()
        };
        await updateStepStatus('Calculate Mastery', 'completed', null, result);
        return result;
      } catch (err: any) {
        await updateStepStatus('Calculate Mastery', 'failed', null, null, err.message);
        throw err;
      }
    });

    context.masteryResults = masteryResults;
    await saveContextSnapshot(context);

    // ============================================================
    // STEP 4: Gap Diagnosis
    // ============================================================
    const diagnosticGaps = await this.step.run('Run Gap Diagnosis', {
      retries: { limit: 3, delay: '2s', backoff: 'exponential' }
    }, async () => {
      await updateStepStatus('Run Gap Diagnosis', 'running', { userId });
      try {
        // Diagnose prerequisite gaps in dependency graph
        const gaps = ['sk_algebra_foundations'];
        await updateStepStatus('Run Gap Diagnosis', 'completed', null, { gaps });
        return { gaps };
      } catch (err: any) {
        await updateStepStatus('Run Gap Diagnosis', 'failed', null, null, err.message);
        throw err;
      }
    });

    context.diagnosticGaps = diagnosticGaps;
    await saveContextSnapshot(context);

    // ============================================================
    // STEP 5: Generate Recommendations
    // ============================================================
    const recResults = await this.step.run('Generate Recommendation', {
      retries: { limit: 3, delay: '2s', backoff: 'exponential' }
    }, async () => {
      await updateStepStatus('Generate Recommendation', 'running', { userId });
      try {
        // Invoke candidate filters, rankers and reasons generators
        const recommendationId = generateId('rec');
        const result = { recommendationId, status: 'generated' };
        await updateStepStatus('Generate Recommendation', 'completed', null, result);
        return result;
      } catch (err: any) {
        await updateStepStatus('Generate Recommendation', 'failed', null, null, err.message);
        throw err;
      }
    });

    context.recResults = recResults;
    await saveContextSnapshot(context);

    // Mark overall workflow execution as completed successfully
    await db.prepare(`
      UPDATE ops_workflows 
      SET status = 'completed', completed_at = ?, updated_at = ?, output_data = ?
      WHERE id = ?
    `).bind(
      new Date().toISOString(), 
      new Date().toISOString(), 
      JSON.stringify(context),
      workflowId
    ).run();

    return context;
  }
}

4.2 Checkpoint Preservation & Engine Details

The Cloudflare Workflows platform implements implicit state checkpointing. When step.run finishes, the return value is serialized and written to persistent Cloudflare state storage.

If the instance crashes during step 3:

  1. Steps 1 and 2 will not execute again because their serialized return values exist in the workflow history.
  2. The orchestrator resumes execution at step 3, passing the cached values of steps 1 and 2 to variables gradingResults and evidenceResults.

5. Integration & Submission API Refactoring

The synchronous path in routes/submission.ts is split at the grading boundary.

5.1 Before/After Workflow Boundaries

[BEFORE - Synchronous Route Processing]
Client POST ➔ Validate ➔ Grade ➔ Write Evidence ➔ Calculate Mastery ➔ Generate Rec ➔ Respond (201)

[AFTER - Asynchronous Orchestration]
Client POST ➔ Validate ➔ Create DB Submission (Status: submitted)
                     ➔ Dispatch Cloudflare Workflow (PostQuizProcessingWorkflow)
                     ➔ Return 202 Accepted immediately

5.2 API Integration Diff Example

Here is how worker/src/routes/submission.ts is refactored. The database execution remains transactionally safe by changing the initial insert status.

diff
-  // 8. Process evidence
-  const evidenceInputs = scoredAnswers.map((sa) => ({
-    submissionId,
-    questionId: sa.questionId,
-    score: sa.score,
-    maxScore: sa.points,
-    answeredAt: now,
-    timeTakenSeconds: timePerQuestion,
-  }));
-
-  const evidenceRecords = await processEvidence(
-    db, orgId, user.id, class_id, evidenceInputs,
-  );
-
-  // 9. Calculate mastery updates
-  const masteryUpdates = await calculateMastery(
-    db, orgId, user.id, class_id, submissionId, evidenceRecords,
-  );
-
-  // 10. Generate new recommendation
-  const classInfo = await db
-    .prepare(`SELECT course_id FROM lrn_classes WHERE id = ?`)
-    .bind(class_id)
-    .first<{ course_id: string }>();
-
-  let newRecommendation = null;
-  if (classInfo) {
-    const recResult = await generateRecommendation(
-      db, orgId, user.id, class_id, classInfo.course_id,
-    );
-    newRecommendation = recResult.recommendation;
-  }
-
-  // Update submission status to graded
-  await db
-    .prepare(
-      `UPDATE lrn_submissions SET status = 'graded', graded_at = ?, updated_at = ? WHERE id = ?`,
-    )
-    .bind(now, now, submissionId)
-    .run();
-
-  // 11. Build response
-  // ... Return full payload with mastery updates
+  // 8. Dispatch Asynchronous Cloudflare Workflow
+  let workflowInstance;
+  try {
+    // Start execution on Cloudflare Workflows binding
+    workflowInstance = await c.env.WORKFLOWS.post_quiz_processing.create({
+      params: {
+        submissionId,
+        orgId,
+        userId: user.id,
+        classId: class_id,
+        courseId: classInfo?.course_id ?? ''
+      }
+    });
+  } catch (wfError: any) {
+    // Fallback: log error and queue submission in pending state
+    console.error('Failed to trigger workflow, queueing manually:', wfError);
+  }
+
+  // 9. Build and return 202 Accepted response immediately
+  const response = {
+    status: "accepted",
+    message: "Bài làm đã được ghi nhận và đang được chấm tự động.",
+    submission: {
+      id: submissionId,
+      attempt_number: attemptNo,
+      status: "submitted"
+    },
+    workflow: {
+      id: workflowInstance?.id ?? null,
+      status: "queued"
+    }
+  };
+
+  return c.json(response, 202);

6. Error Handling, Transactions, & Recovery

Asynchronous execution introduces challenges with concurrency, locking, and data consistency.

6.1 D1 Transaction Boundaries

Cloudflare D1 runs SQLite under the hood. SQLite locks the entire database file during write transactions. To prevent transaction locks and timeouts:

  1. Keep Transactions Short: Do not bundle the entire workflow into a single SQLite transaction.
  2. Step Isolation: Each step executes separate, isolated transactions. Step 2 processes and commits its inserts before Step 3 runs its query.
  3. Idempotence with INSERT OR IGNORE: If a step crashes halfway and gets retried, the query uses unique constraints (e.g. PRIMARY KEY(id) or UNIQUE(submission_id, question_id)) with INSERT OR IGNORE (or UPSERT) to prevent double-inserting.

6.2 Resumability Procedure

When a workflow fails, it transitions to failed state. The manual recovery workflow functions as follows:

mermaid
stateDiagram-v2
    StateFailed: Workflow Status = 'failed'
    StateLoading: Reload Context from ops_workflow_context
    StateResume: Resume from Failed Step
    StateRunning: Status = 'running'

    [*] --> StateFailed
    StateFailed --> StateLoading: Administrator triggers /resume API
    StateLoading --> StateResume: Resolve DB lock / code bug
    StateResume --> StateRunning: Re-execute step logic
    StateRunning --> [*]: Workflow Completes
  1. Verification: The administrator queries the error details via /api/workflows/:id/logs.
  2. Context Resolution: The administrator POSTs to /api/workflows/:id/resume.
  3. Re-trigger: Hono matches the failed workflow instance ID and calls:
    typescript
    const instance = await env.WORKFLOWS.post_quiz_processing.get(id);
    await instance.resume();
  4. Resumption: The Cloudflare Workflows runtime re-reads the serialized step context, skips the completed checkpoints, and starts execution directly from the step that failed.

Developed by Hanoi Agents for the EduOne Platform.