Notification Trigger Lambda

The official DocuFlow AI architecture uses docuflow-dev-notification-trigger-lambda to publish workflow failure and REVIEW_REQUIRED notifications to the system SNS topic. SNS then distributes the message to confirmed email subscriptions or other configured consumers.

1. Create the function

  1. Open AWS Lambda and choose Create function.
  2. Select Author from scratch.
  3. Enter docuflow-dev-notification-trigger-lambda as the function name.
  4. Select the same supported Node.js runtime used by the other project Lambdas and the arm64 architecture.
  5. Select the existing execution role docuflow-dev-notification-lambda-role.
  6. Create the function and set the timeout to 30 seconds.

2. Configure environment variables

Add this required variable:

KeyValue
DOCUFLOW_DEV_NOTIFICATION_TOPIC_ARNARN of docuflow-dev-notification-system-alerts-topic

The execution role must allow sns:Publish on this topic and CloudWatch Logs write access.

3. Deploy the code

Replace the default index.mjs code with the current project source and choose Deploy.

import { PublishCommand, SNSClient } from "@aws-sdk/client-sns";

const snsClient = new SNSClient({});

const TOPIC_ARN =
  process.env.DOCUFLOW_DEV_NOTIFICATION_TOPIC_ARN ||
  process.env.SNS_TOPIC_ARN;
const NOTIFIABLE_STATUSES = new Set(["REVIEW_REQUIRED", "FAILED"]);

export const handler = async (event) => {
  const document = resolveDocument(event);
  const documentId = document.documentId || event?.documentId || null;
  const userId = document.userId || event?.userId || null;
  const status = document.status || event?.status || null;

  log("INFO", {
    documentId,
    userId,
    status,
    message: "Notification evaluation started",
  });

  if (!documentId || !userId || !status) {
    throw createError(
      "INVALID_NOTIFICATION_INPUT",
      "documentId, userId, and status are required"
    );
  }

  if (!NOTIFIABLE_STATUSES.has(status)) {
    log("INFO", {
      documentId,
      userId,
      status,
      message: "Notification skipped for non-alert status",
    });
    return {
      notified: false,
      documentId,
      status,
      reason: "STATUS_DOES_NOT_REQUIRE_NOTIFICATION",
    };
  }

  if (!TOPIC_ARN) {
    throw createError(
      "MISSING_NOTIFICATION_TOPIC_ARN",
      "DOCUFLOW_DEV_NOTIFICATION_TOPIC_ARN environment variable is missing"
    );
  }

  const notification = buildNotification(document, {
    documentId,
    userId,
    status,
  });

  try {
    const result = await snsClient.send(
      new PublishCommand({
        TopicArn: TOPIC_ARN,
        Subject: notification.subject,
        Message: JSON.stringify(notification.message, null, 2),
        MessageAttributes: {
          documentId: {
            DataType: "String",
            StringValue: documentId,
          },
          status: {
            DataType: "String",
            StringValue: status,
          },
          severity: {
            DataType: "String",
            StringValue: notification.message.severity,
          },
        },
      })
    );

    log("INFO", {
      documentId,
      userId,
      status,
      snsMessageId: result.MessageId || null,
      message: "Notification published",
    });

    return {
      notified: true,
      documentId,
      status,
      snsMessageId: result.MessageId || null,
    };
  } catch (error) {
    log("ERROR", {
      documentId,
      userId,
      status,
      errorName: error?.name,
      errorMessage: error?.message,
      message: "Notification publish failed",
    });
    throw createError(
      "NOTIFICATION_FAILED",
      "Could not publish the DocuFlow notification",
      error
    );
  }
};

function resolveDocument(event) {
  const candidate =
    event?.finalDocument ||
    event?.document ||
    event?.Payload ||
    event?.payload ||
    event;
  return isRecord(candidate) ? candidate : {};
}

function buildNotification(document, context) {
  const reviewReasonCodes = Array.isArray(
    document?.review?.reviewReasonCodes
  )
    ? document.review.reviewReasonCodes
    : [];
  const error = isRecord(document?.error) ? document.error : {};
  const isFailure = context.status === "FAILED";

  return {
    subject: isFailure
      ? `[DocuFlow AI] Processing failed: ${context.documentId}`
      : `[DocuFlow AI] Review required: ${context.documentId}`,
    message: {
      project: "DocuFlowAI",
      environment: "dev",
      severity: isFailure ? "ERROR" : "WARNING",
      documentId: context.documentId,
      userId: context.userId,
      status: context.status,
      reviewReasonCodes,
      errorCode: error.errorCode || null,
      errorStage: error.errorStage || null,
      occurredAt: new Date().toISOString(),
      action: isFailure
        ? "Inspect Step Functions execution history and CloudWatch logs."
        : "Open the reviewer interface and verify the extracted fields.",
    },
  };
}

function createError(code, message, cause = null) {
  const error = new Error(message, cause ? { cause } : undefined);
  error.name = code;
  error.code = code;
  return error;
}

function isRecord(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function log(level, data) {
  console.log(
    JSON.stringify({
      level,
      service: "docuflow-dev-notification-trigger-lambda",
      ...data,
    })
  );
}

4. Test REVIEW_REQUIRED notification

Create a Lambda test event:

{
  "documentId": "doc-review-001",
  "userId": "user-001",
  "status": "REVIEW_REQUIRED",
  "review": {
    "reviewStatus": "PENDING",
    "reviewReasonCodes": ["LOW_CONFIDENCE", "MISSING_INVOICE_DATE"]
  },
  "error": {
    "errorCode": null,
    "errorStage": null
  }
}

Expected result: notified is true, an SNS MessageId is returned, and the confirmed subscriber receives a review-required email.

5. Test failure notification

{
  "documentId": "doc-failed-001",
  "userId": "user-001",
  "status": "FAILED",
  "review": {
    "reviewStatus": "PENDING",
    "reviewReasonCodes": []
  },
  "error": {
    "errorCode": "TEXTRACT_FAILED",
    "errorStage": "TEXTRACT"
  }
}

Expected result: the function publishes an error notification without exposing document contents, extracted financial fields, or secrets.

6. Step Functions integration

The processing state machine invokes this Lambda from two states:

  • PublishSNSAlert for REVIEW_REQUIRED documents.
  • PublishFailureAlert after failure metadata has been persisted.

When deploying with SAM, map ${NotificationTriggerFunctionArn} to this function ARN through DefinitionSubstitutions.