# Docuseal Submission Completion Processing: A Step-by-Step Code Analysis

> Analyze Docuseal submission completion processing step-by-step. Learn how the ProcessSubmitterCompletionJob handles PDF generation, audit trails, emails, and webhooks.

- Repository: [DocuSeal/docuseal](https://github.com/docusealco/docuseal)
- Tags: deep-dive
- Published: 2026-05-05

---

**When a submitter finishes a Docuseal form, the `ProcessSubmitterCompletionJob` Sidekiq job orchestrates PDF generation, audit trails, email notifications, and webhook deliveries to finalize the submission.**

In the `docusealco/docuseal` open-source document signing platform, submission completion processing transforms raw form data into finalized, auditable records. This critical workflow runs entirely in the background to keep the web interface responsive while handling document generation, persistence, and external integrations. Understanding this pipeline is essential for developers customizing notifications, debugging webhook delivery, or scaling the application.

## Orchestration Architecture

All submission completion processing happens inside `ProcessSubmitterCompletionJob`, located at [`app/jobs/process_submitter_completion_job.rb`](https://github.com/docusealco/docuseal/blob/main/app/jobs/process_submitter_completion_job.rb). This Sidekiq background job receives a `submitter_id` parameter and executes an eleven-step pipeline that converts a submitter's finalized data into permanent artifacts.

The job maintains strict separation of concerns by delegating heavy PDF manipulation to service objects in `lib/submissions/`, while handling database persistence and notification orchestration itself. This architecture ensures that HTTP requests return immediately, with the heavy lifting deferred to background workers.

## Step-by-Step Execution Flow

### Loading and Snapshot Creation

The job begins by loading the submitter record and creating a durable snapshot of the completion state. At lines 7-8 in [`app/jobs/process_submitter_completion_job.rb`](https://github.com/docusealco/docuseal/blob/main/app/jobs/process_submitter_completion_job.rb), the job fetches the record:

```ruby
submitter = Submitter.find(params['submitter_id'])

```

Immediately after, the `create_completed_submitter!` method (lines 34-68) ensures a `CompletedSubmitter` row exists. This table stores immutable metadata about the final submission state, including the verification method and SMS usage counts. The implementation uses `find_or_initialize_by` and rescues `ActiveRecord::RecordNotUnique` (line 68) to guarantee idempotency during race conditions.

### PDF Generation Pipeline

The system generates three distinct PDF artifacts depending on configuration and submission state:

1. **Result PDF**: The `Submissions::EnsureResultGenerated` service (called at line 13) merges the submitter’s filled fields into the template PDF, creating the final signed document.

2. **Combined PDF**: When all submitters for a submission are complete (determined at line 11 via `is_all_completed`), and the account has enabled `AccountConfig::COMBINE_PDF_RESULT_KEY`, the job calls `Submissions::EnsureCombinedGenerated` (lines 15-18) to create a single PDF containing every submitter's results.

3. **Audit-Trail PDF**: Regardless of other settings, the job always generates a tamper-evident audit trail by calling `Submissions::EnsureAuditGenerated` at lines 20-21.

```ruby

# From ProcessSubmitterCompletionJob#perform

is_all_completed = submission.submitters.all?(&:completed_at?)
Submissions::EnsureResultGenerated.call(submitter)

if is_all_completed && account_config.combine_pdf_result_enabled?
  Submissions::EnsureCombinedGenerated.call(submission)
end

Submissions::EnsureAuditGenerated.call(submission)

```

### Document Persistence

For each attachment with a SHA-256 hash, the job creates or retrieves a `CompletedDocument` record via `create_completed_documents!` (lines 72-78). This deduplication mechanism stores document references by hash, preventing duplicate storage of identical files across multiple submissions.

### Completion Detection and Sequential Workflows

The job checks whether the current submitter represents the final pending signature (line 11). If not all submitters are finished and the submission uses preserved ordering (`submitters_order == 'preserved'`), the job triggers the next participant via `enqueue_next_submitter_request_notification` (lines 27-28 and method implementation at lines 60-92).

### Notification and Webhook Delivery

Once artifacts are generated, the notification pipeline executes in two phases:

**Email Notifications**: The `enqueue_completed_emails` method (lines 22-23 and 30-27) builds recipient lists based on account settings, including owners, BCC addresses, and copy-email configurations. It dispatches `SubmitterMailer.completed_email` to each recipient.

**Webhook Requests**: The `enqueue_completed_webhooks` method (lines 80-99) iterates through account webhooks listening for `form.completed` or `submission.completed` events, queuing `SendFormCompletedWebhookRequestJob` or `SendSubmissionCompletedWebhookRequestJob` for asynchronous delivery.

## Idempotency and Error Handling

The submission completion processing implements several safeguards against race conditions and duplicate processing:

- **Record uniqueness**: The `create_completed_submitter!` method handles `ActiveRecord::RecordNotUnique` exceptions to survive concurrent job executions.
- **Conditional PDF generation**: Combined PDF creation only occurs when `is_all_completed` is true, preventing partial artifacts.
- **Automatic retries**: Sidekiq handles transient failures automatically according to the configured retry policy, ensuring eventual consistency.

## Triggering the Completion Process

When a user submits their final field, the controller enqueues the background job. While the frontend posts signature data to the submitter completion endpoint, the background job is initiated with:

```ruby

# Simplified example from the submission flow

submitter.complete!
ProcessSubmitterCompletionJob.perform_async(
  'submitter_id' => submitter.id,
  'send_invitation_email' => true
)

```

The controller at [`app/controllers/submissions_controller.rb`](https://github.com/docusealco/docuseal/blob/main/app/controllers/submissions_controller.rb) preloads submission data for the UI, checking completion status before rendering:

```ruby
def show
  @submission = Submissions.preload_with_pages(@submission)

  unless @submission.submitters.all?(&:completed_at?)
    ActiveRecord::Associations::Preloader.new(
      records: [@submission],
      associations: [{ submitters: :start_form_submission_events }]
    ).call
  end

  render :show, layout: 'plain'
end

```

## Summary

- **Background Processing**: The `ProcessSubmitterCompletionJob` in [`app/jobs/process_submitter_completion_job.rb`](https://github.com/docusealco/docuseal/blob/main/app/jobs/process_submitter_completion_job.rb) handles all submission completion processing asynchronously via Sidekiq.
- **PDF Generation**: The system produces result PDFs, optional combined PDFs (when `AccountConfig::COMBINE_PDF_RESULT_KEY` is enabled), and mandatory audit-trail PDFs through dedicated service objects.
- **Data Persistence**: `CompletedSubmitter` snapshots and `CompletedDocument` records provide immutable references to finalized states and deduplicated file storage.
- **Notification Pipeline**: Email delivery and webhook requests (`SendFormCompletedWebhookRequestJob` / `SendSubmissionCompletedWebhookRequestJob`) execute only after successful artifact generation.
- **Sequential Support**: When submitter order is preserved and submissions remain incomplete, the job automatically queues invitations for the next participant.

## Frequently Asked Questions

### What happens if the ProcessSubmitterCompletionJob fails mid-execution?

The job is designed to be idempotent. It uses `find_or_initialize_by` patterns and rescues `ActiveRecord::RecordNotUnique` exceptions (line 68) to handle race conditions gracefully. Sidekiq automatically retries failed jobs according to the configured retry policy, ensuring that temporary network issues or database locks do not result in lost completion data.

### Where are the generated PDFs stored during submission completion processing?

The PDF generation logic resides in `lib/submissions/` (specifically [`ensure_result_generated.rb`](https://github.com/docusealco/docuseal/blob/main/ensure_result_generated.rb), [`ensure_combined_generated.rb`](https://github.com/docusealco/docuseal/blob/main/ensure_combined_generated.rb), and [`ensure_audit_generated.rb`](https://github.com/docusealco/docuseal/blob/main/ensure_audit_generated.rb)). These service objects write to the configured Active Storage backend. The resulting file references are stored in `CompletedDocument` records indexed by SHA-256 hash for deduplication.

### How does Docuseal handle multiple submitters in sequential signing workflows?

When a submission has multiple submitters with preserved ordering, the job checks `is_all_completed` at line 11. If additional submitters remain pending, `enqueue_next_submitter_request_notification` (lines 27-28) automatically sends invitation emails to the next participant in sequence. Combined PDF generation is deferred until all submitters have completed their portions.

### Can I disable specific notifications during the completion process?

Yes. The notification pipeline respects account configuration settings. The `enqueue_completed_emails` method checks for `documents_copy_email_enabled` and other preferences before dispatching `SubmitterMailer.completed_email`. Similarly, webhooks only fire for URLs explicitly configured in the account webhook settings for `form.completed` or `submission.completed` events.