From 63c0e2f72b53ca2d38851930a4bcd1d5c8b52321 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 3 Feb 2026 08:13:10 -0800
Subject: [PATCH 1/3] Documentation: clarify workflow placeholders docs
---
docs/usage.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/usage.md b/docs/usage.md
index f652164da..1e339b61e 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -562,8 +562,8 @@ you may want to adjust these settings to prevent abuse.
#### Workflow placeholders
-Titles can be assigned by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
-This allows for complex logic to be used to generate the title, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
+Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
+This allows for complex logic to be used, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11).
The template is provided as a string.
@@ -586,7 +586,7 @@ applied. You can use the following placeholders in the template with any trigger
- `{{added_time}}`: added time in HH:MM format
- `{{original_filename}}`: original file name without extension
- `{{filename}}`: current file name without extension
-- `{{doc_title}}`: current document title
+- `{{doc_title}}`: current document title (cannot be used in title assignment)
The following placeholders are only available for "added" or "updated" triggers
From e45fca475aed4538aeb8a84157ed725a93cc61ef Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 3 Feb 2026 09:10:07 -0800
Subject: [PATCH 2/3] Feature: password removal workflow action (#11665)
---
.../workflow-edit-dialog.component.html | 18 ++
.../workflow-edit-dialog.component.spec.ts | 29 +++
.../workflow-edit-dialog.component.ts | 20 ++
src-ui/src/app/data/workflow-action.ts | 3 +
...ion_passwords_alter_workflowaction_type.py | 38 ++++
src/documents/models.py | 13 ++
src/documents/serialisers.py | 18 ++
src/documents/signals/handlers.py | 3 +
src/documents/tests/test_api_workflows.py | 58 ++++++
src/documents/tests/test_workflows.py | 192 ++++++++++++++++++
src/documents/workflows/actions.py | 73 +++++++
11 files changed, 465 insertions(+)
create mode 100644 src/documents/migrations/0009_workflowaction_passwords_alter_workflowaction_type.py
diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
index 5af53e79d..7f086ec63 100644
--- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
+++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
@@ -430,6 +430,24 @@
}
+ @case (WorkflowActionType.PasswordRemoval) {
+
+
+
+ One password per line. The workflow will try them in order until one succeeds.
+
+
+
+
+ }
}
diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts
index ac8a5d2c7..070e5124f 100644
--- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts
+++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts
@@ -3,6 +3,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
+ FormArray,
FormControl,
FormGroup,
FormsModule,
@@ -994,4 +995,32 @@ describe('WorkflowEditDialogComponent', () => {
component.removeSelectedCustomField(3, formGroup)
expect(formGroup.get('assign_custom_fields').value).toEqual([])
})
+
+ it('should handle parsing of passwords from array to string and back on save', () => {
+ const passwordAction: WorkflowAction = {
+ id: 1,
+ type: WorkflowActionType.PasswordRemoval,
+ passwords: ['pass1', 'pass2'],
+ }
+ component.object = {
+ name: 'Workflow with Passwords',
+ id: 1,
+ order: 1,
+ enabled: true,
+ triggers: [],
+ actions: [passwordAction],
+ }
+ component.ngOnInit()
+
+ const formActions = component.objectForm.get('actions') as FormArray
+ expect(formActions.value[0].passwords).toBe('pass1\npass2')
+ formActions.at(0).get('passwords').setValue('pass1\npass2\npass3')
+ component.save()
+
+ expect(component.objectForm.get('actions').value[0].passwords).toEqual([
+ 'pass1',
+ 'pass2',
+ 'pass3',
+ ])
+ })
})
diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
index 94d8318e0..37d8bef0d 100644
--- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
+++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
@@ -139,6 +139,10 @@ export const WORKFLOW_ACTION_OPTIONS = [
id: WorkflowActionType.Webhook,
name: $localize`Webhook`,
},
+ {
+ id: WorkflowActionType.PasswordRemoval,
+ name: $localize`Password removal`,
+ },
]
export enum TriggerFilterType {
@@ -1202,11 +1206,25 @@ export class WorkflowEditDialogComponent
headers: new FormControl(action.webhook?.headers),
include_document: new FormControl(!!action.webhook?.include_document),
}),
+ passwords: new FormControl(
+ this.formatPasswords(action.passwords ?? [])
+ ),
}),
{ emitEvent }
)
}
+ private formatPasswords(passwords: string[] = []): string {
+ return passwords.join('\n')
+ }
+
+ private parsePasswords(value: string = ''): string[] {
+ return value
+ .split(/[\n,]+/)
+ .map((entry) => entry.trim())
+ .filter((entry) => entry.length > 0)
+ }
+
private updateAllTriggerActionFields(emitEvent: boolean = false) {
this.triggerFields.clear({ emitEvent: false })
this.object?.triggers.forEach((trigger) => {
@@ -1331,6 +1349,7 @@ export class WorkflowEditDialogComponent
headers: null,
include_document: false,
},
+ passwords: [],
}
this.object.actions.push(action)
this.createActionField(action)
@@ -1367,6 +1386,7 @@ export class WorkflowEditDialogComponent
if (action.type !== WorkflowActionType.Email) {
action.email = null
}
+ action.passwords = this.parsePasswords(action.passwords as any)
})
super.save()
}
diff --git a/src-ui/src/app/data/workflow-action.ts b/src-ui/src/app/data/workflow-action.ts
index 06c46806e..ff1509693 100644
--- a/src-ui/src/app/data/workflow-action.ts
+++ b/src-ui/src/app/data/workflow-action.ts
@@ -5,6 +5,7 @@ export enum WorkflowActionType {
Removal = 2,
Email = 3,
Webhook = 4,
+ PasswordRemoval = 5,
}
export interface WorkflowActionEmail extends ObjectWithId {
@@ -97,4 +98,6 @@ export interface WorkflowAction extends ObjectWithId {
email?: WorkflowActionEmail
webhook?: WorkflowActionWebhook
+
+ passwords?: string[]
}
diff --git a/src/documents/migrations/0009_workflowaction_passwords_alter_workflowaction_type.py b/src/documents/migrations/0009_workflowaction_passwords_alter_workflowaction_type.py
new file mode 100644
index 000000000..ae3fef79f
--- /dev/null
+++ b/src/documents/migrations/0009_workflowaction_passwords_alter_workflowaction_type.py
@@ -0,0 +1,38 @@
+# Generated by Django 5.2.7 on 2025-12-29 03:56
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("documents", "0008_sharelinkbundle"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="workflowaction",
+ name="passwords",
+ field=models.JSONField(
+ blank=True,
+ help_text="Passwords to try when removing PDF protection. Separate with commas or new lines.",
+ null=True,
+ verbose_name="passwords",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="type",
+ field=models.PositiveIntegerField(
+ choices=[
+ (1, "Assignment"),
+ (2, "Removal"),
+ (3, "Email"),
+ (4, "Webhook"),
+ (5, "Password removal"),
+ ],
+ default=1,
+ verbose_name="Workflow Action Type",
+ ),
+ ),
+ ]
diff --git a/src/documents/models.py b/src/documents/models.py
index 2e187e98c..5a813f9b5 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -1405,6 +1405,10 @@ class WorkflowAction(models.Model):
4,
_("Webhook"),
)
+ PASSWORD_REMOVAL = (
+ 5,
+ _("Password removal"),
+ )
type = models.PositiveIntegerField(
_("Workflow Action Type"),
@@ -1634,6 +1638,15 @@ class WorkflowAction(models.Model):
verbose_name=_("webhook"),
)
+ passwords = models.JSONField(
+ _("passwords"),
+ null=True,
+ blank=True,
+ help_text=_(
+ "Passwords to try when removing PDF protection. Separate with commas or new lines.",
+ ),
+ )
+
class Meta:
verbose_name = _("workflow action")
verbose_name_plural = _("workflow actions")
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index cfd2ad3cf..5fd159772 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -2627,6 +2627,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
"remove_change_groups",
"email",
"webhook",
+ "passwords",
]
def validate(self, attrs):
@@ -2683,6 +2684,23 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
"Webhook data is required for webhook actions",
)
+ if (
+ "type" in attrs
+ and attrs["type"] == WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL
+ ):
+ passwords = attrs.get("passwords")
+ # ensure passwords is a non-empty list of non-empty strings
+ if (
+ passwords is None
+ or not isinstance(passwords, list)
+ or len(passwords) == 0
+ or any(not isinstance(pw, str) for pw in passwords)
+ or any(len(pw.strip()) == 0 for pw in passwords)
+ ):
+ raise serializers.ValidationError(
+ "Passwords are required for password removal actions",
+ )
+
return attrs
diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py
index 8ef5cad04..47ebab6f5 100644
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -48,6 +48,7 @@ from documents.permissions import get_objects_for_user_owner_aware
from documents.templating.utils import convert_format_str_to_template_format
from documents.workflows.actions import build_workflow_action_context
from documents.workflows.actions import execute_email_action
+from documents.workflows.actions import execute_password_removal_action
from documents.workflows.actions import execute_webhook_action
from documents.workflows.mutations import apply_assignment_to_document
from documents.workflows.mutations import apply_assignment_to_overrides
@@ -831,6 +832,8 @@ def run_workflows(
logging_group,
original_file,
)
+ elif action.type == WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL:
+ execute_password_removal_action(action, document, logging_group)
if not use_overrides:
# limit title to 128 characters
diff --git a/src/documents/tests/test_api_workflows.py b/src/documents/tests/test_api_workflows.py
index a11cb490a..f07b2b60c 100644
--- a/src/documents/tests/test_api_workflows.py
+++ b/src/documents/tests/test_api_workflows.py
@@ -838,3 +838,61 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.action.refresh_from_db()
self.assertEqual(self.action.assign_title, "Patched Title")
+
+ def test_password_action_passwords_field(self):
+ """
+ GIVEN:
+ - Nothing
+ WHEN:
+ - A workflow password removal action is created with passwords set
+ THEN:
+ - The passwords field is correctly stored and retrieved
+ """
+ passwords = ["password1", "password2", "password3"]
+ response = self.client.post(
+ "/api/workflow_actions/",
+ json.dumps(
+ {
+ "type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ "passwords": passwords,
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(response.data["passwords"], passwords)
+
+ def test_password_action_invalid_passwords_field(self):
+ """
+ GIVEN:
+ - Nothing
+ WHEN:
+ - A workflow password removal action is created with invalid passwords field
+ THEN:
+ - The required validation error is raised
+ """
+ for payload in [
+ {"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL},
+ {
+ "type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ "passwords": "",
+ },
+ {
+ "type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ "passwords": [],
+ },
+ {
+ "type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ "passwords": ["", "password2"],
+ },
+ ]:
+ response = self.client.post(
+ "/api/workflow_actions/",
+ json.dumps(payload),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn(
+ "Passwords are required",
+ str(response.data["non_field_errors"][0]),
+ )
diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py
index 964d7eef6..1cd0a9826 100644
--- a/src/documents/tests/test_workflows.py
+++ b/src/documents/tests/test_workflows.py
@@ -2,6 +2,7 @@ import datetime
import json
import shutil
import socket
+import tempfile
from datetime import timedelta
from pathlib import Path
from typing import TYPE_CHECKING
@@ -60,6 +61,7 @@ from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
+from documents.workflows.actions import execute_password_removal_action
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
@@ -3722,6 +3724,196 @@ class TestWorkflows(
mock_post.assert_called_once()
+ @mock.patch("documents.bulk_edit.remove_password")
+ def test_password_removal_action_attempts_multiple_passwords(
+ self,
+ mock_remove_password,
+ ):
+ """
+ GIVEN:
+ - Workflow password removal action
+ - Multiple passwords provided
+ WHEN:
+ - Document updated triggering the workflow
+ THEN:
+ - Password removal is attempted until one succeeds
+ """
+ doc = Document.objects.create(
+ title="Protected",
+ checksum="pw-checksum",
+ )
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ passwords="wrong, right\n extra ",
+ )
+ workflow = Workflow.objects.create(name="Password workflow")
+ workflow.triggers.add(trigger)
+ workflow.actions.add(action)
+
+ mock_remove_password.side_effect = [
+ ValueError("wrong password"),
+ "OK",
+ ]
+
+ run_workflows(trigger.type, doc)
+
+ assert mock_remove_password.call_count == 2
+ mock_remove_password.assert_has_calls(
+ [
+ mock.call(
+ [doc.id],
+ password="wrong",
+ update_document=True,
+ user=doc.owner,
+ ),
+ mock.call(
+ [doc.id],
+ password="right",
+ update_document=True,
+ user=doc.owner,
+ ),
+ ],
+ )
+
+ @mock.patch("documents.bulk_edit.remove_password")
+ def test_password_removal_action_fails_without_correct_password(
+ self,
+ mock_remove_password,
+ ):
+ """
+ GIVEN:
+ - Workflow password removal action
+ - No correct password provided
+ WHEN:
+ - Document updated triggering the workflow
+ THEN:
+ - Password removal is attempted for all passwords and fails
+ """
+ doc = Document.objects.create(
+ title="Protected",
+ checksum="pw-checksum-2",
+ )
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ passwords=" \n , ",
+ )
+ workflow = Workflow.objects.create(name="Password workflow missing passwords")
+ workflow.triggers.add(trigger)
+ workflow.actions.add(action)
+
+ run_workflows(trigger.type, doc)
+
+ mock_remove_password.assert_not_called()
+
+ @mock.patch("documents.bulk_edit.remove_password")
+ def test_password_removal_action_skips_without_passwords(
+ self,
+ mock_remove_password,
+ ):
+ """
+ GIVEN:
+ - Workflow password removal action with no passwords
+ WHEN:
+ - Workflow is run
+ THEN:
+ - Password removal is not attempted
+ """
+ doc = Document.objects.create(
+ title="Protected",
+ checksum="pw-checksum-2",
+ )
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ passwords="",
+ )
+ workflow = Workflow.objects.create(name="Password workflow missing passwords")
+ workflow.triggers.add(trigger)
+ workflow.actions.add(action)
+
+ run_workflows(trigger.type, doc)
+
+ mock_remove_password.assert_not_called()
+
+ @mock.patch("documents.bulk_edit.remove_password")
+ def test_password_removal_consumable_document_deferred(
+ self,
+ mock_remove_password,
+ ):
+ """
+ GIVEN:
+ - Workflow password removal action
+ - Simulated consumption trigger (a ConsumableDocument is used)
+ WHEN:
+ - Document consumption is finished
+ THEN:
+ - Password removal is attempted
+ """
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
+ passwords="first, second",
+ )
+
+ temp_dir = Path(tempfile.mkdtemp())
+ original_file = temp_dir / "file.pdf"
+ original_file.write_bytes(b"pdf content")
+ consumable = ConsumableDocument(
+ source=DocumentSource.ApiUpload,
+ original_file=original_file,
+ )
+
+ execute_password_removal_action(action, consumable, logging_group=None)
+
+ mock_remove_password.assert_not_called()
+
+ mock_remove_password.side_effect = [
+ ValueError("bad password"),
+ "OK",
+ ]
+
+ doc = Document.objects.create(
+ checksum="pw-checksum-consumed",
+ title="Protected",
+ )
+
+ document_consumption_finished.send(
+ sender=self.__class__,
+ document=doc,
+ )
+
+ assert mock_remove_password.call_count == 2
+ mock_remove_password.assert_has_calls(
+ [
+ mock.call(
+ [doc.id],
+ password="first",
+ update_document=True,
+ user=doc.owner,
+ ),
+ mock.call(
+ [doc.id],
+ password="second",
+ update_document=True,
+ user=doc.owner,
+ ),
+ ],
+ )
+
+ # ensure handler disconnected after first run
+ document_consumption_finished.send(
+ sender=self.__class__,
+ document=doc,
+ )
+ assert mock_remove_password.call_count == 2
+
class TestWebhookSend:
def test_send_webhook_data_or_json(
diff --git a/src/documents/workflows/actions.py b/src/documents/workflows/actions.py
index a61b9930e..442bc0abe 100644
--- a/src/documents/workflows/actions.py
+++ b/src/documents/workflows/actions.py
@@ -1,4 +1,5 @@
import logging
+import re
from pathlib import Path
from django.conf import settings
@@ -14,6 +15,7 @@ from documents.models import Document
from documents.models import DocumentType
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
+from documents.signals import document_consumption_finished
from documents.templating.workflows import parse_w_workflow_placeholders
from documents.workflows.webhooks import send_webhook
@@ -265,3 +267,74 @@ def execute_webhook_action(
f"Error occurred sending webhook: {e}",
extra={"group": logging_group},
)
+
+
+def execute_password_removal_action(
+ action: WorkflowAction,
+ document: Document | ConsumableDocument,
+ logging_group,
+) -> None:
+ """
+ Try to remove a password from a document using the configured list.
+ """
+ passwords = action.passwords
+ if not passwords:
+ logger.warning(
+ "Password removal action %s has no passwords configured",
+ action.pk,
+ extra={"group": logging_group},
+ )
+ return
+
+ passwords = [
+ password.strip()
+ for password in re.split(r"[,\n]", passwords)
+ if password.strip()
+ ]
+
+ if isinstance(document, ConsumableDocument):
+ # hook the consumption-finished signal to attempt password removal later
+ def handler(sender, **kwargs):
+ consumed_document: Document = kwargs.get("document")
+ if consumed_document is not None:
+ execute_password_removal_action(
+ action,
+ consumed_document,
+ logging_group,
+ )
+ document_consumption_finished.disconnect(handler)
+
+ document_consumption_finished.connect(handler, weak=False)
+ return
+
+ # import here to avoid circular dependency
+ from documents.bulk_edit import remove_password
+
+ for password in passwords:
+ try:
+ remove_password(
+ [document.id],
+ password=password,
+ update_document=True,
+ user=document.owner,
+ )
+ logger.info(
+ "Removed password from document %s using workflow action %s",
+ document.pk,
+ action.pk,
+ extra={"group": logging_group},
+ )
+ return
+ except ValueError as e:
+ logger.warning(
+ "Password removal failed for document %s with supplied password: %s",
+ document.pk,
+ e,
+ extra={"group": logging_group},
+ )
+
+ logger.error(
+ "Password removal failed for document %s after trying all provided passwords",
+ document.pk,
+ extra={"group": logging_group},
+ )
From d0c02e7a8d423b065566b62b7d771562c57e0e8a Mon Sep 17 00:00:00 2001
From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:33:37 +0000
Subject: [PATCH 3/3] Auto translate strings
---
src-ui/messages.xlf | 58 +++++++++----
src/locale/en_US/LC_MESSAGES/django.po | 108 ++++++++++++++-----------
2 files changed, 104 insertions(+), 62 deletions(-)
diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf
index e66ad654b..726625ff6 100644
--- a/src-ui/messages.xlf
+++ b/src-ui/messages.xlf
@@ -5359,6 +5359,27 @@
429
+
+ One password per line. The workflow will try them in order until one succeeds.
+
+ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
+ 436,438
+
+
+
+ Passwords
+
+ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
+ 441
+
+
+
+ Passwords are stored in plain text. Use with caution.
+
+ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
+ 445
+
+ Consume Folder
@@ -5454,109 +5475,116 @@
140
+
+ Password removal
+
+ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
+ 144
+
+ Has any of these tagssrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 209
+ 213Has all of these tagssrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 216
+ 220Does not have these tagssrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 223
+ 227Has any of these correspondentssrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 230
+ 234Has correspondentsrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 238
+ 242Does not have correspondentssrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 246
+ 250Has document typesrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 254
+ 258Has any of these document typessrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 262
+ 266Does not have document typessrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 270
+ 274Has storage pathsrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 278
+ 282Has any of these storage pathssrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 286
+ 290Does not have storage pathssrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 294
+ 298Matches custom field querysrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 302
+ 306Create new workflowsrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 531
+ 535Edit workflowsrc/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 535
+ 539
diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po
index 39678bdee..eca080cc6 100644
--- a/src/locale/en_US/LC_MESSAGES/django.po
+++ b/src/locale/en_US/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-01-31 17:12+0000\n"
+"POT-Creation-Date: 2026-02-03 17:32+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -89,7 +89,7 @@ msgstr ""
msgid "Automatic"
msgstr ""
-#: documents/models.py:66 documents/models.py:444 documents/models.py:1646
+#: documents/models.py:66 documents/models.py:444 documents/models.py:1659
#: paperless_mail/models.py:23 paperless_mail/models.py:143
msgid "name"
msgstr ""
@@ -252,7 +252,7 @@ msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:313 documents/models.py:688 documents/models.py:742
-#: documents/models.py:1689
+#: documents/models.py:1702
msgid "document"
msgstr ""
@@ -1089,183 +1089,197 @@ msgid "Webhook"
msgstr ""
#: documents/models.py:1410
+msgid "Password removal"
+msgstr ""
+
+#: documents/models.py:1414
msgid "Workflow Action Type"
msgstr ""
-#: documents/models.py:1415 documents/models.py:1648
+#: documents/models.py:1419 documents/models.py:1661
#: paperless_mail/models.py:145
msgid "order"
msgstr ""
-#: documents/models.py:1418
+#: documents/models.py:1422
msgid "assign title"
msgstr ""
-#: documents/models.py:1422
+#: documents/models.py:1426
msgid "Assign a document title, must be a Jinja2 template, see documentation."
msgstr ""
-#: documents/models.py:1430 paperless_mail/models.py:274
+#: documents/models.py:1434 paperless_mail/models.py:274
msgid "assign this tag"
msgstr ""
-#: documents/models.py:1439 paperless_mail/models.py:282
+#: documents/models.py:1443 paperless_mail/models.py:282
msgid "assign this document type"
msgstr ""
-#: documents/models.py:1448 paperless_mail/models.py:296
+#: documents/models.py:1452 paperless_mail/models.py:296
msgid "assign this correspondent"
msgstr ""
-#: documents/models.py:1457
+#: documents/models.py:1461
msgid "assign this storage path"
msgstr ""
-#: documents/models.py:1466
+#: documents/models.py:1470
msgid "assign this owner"
msgstr ""
-#: documents/models.py:1473
+#: documents/models.py:1477
msgid "grant view permissions to these users"
msgstr ""
-#: documents/models.py:1480
+#: documents/models.py:1484
msgid "grant view permissions to these groups"
msgstr ""
-#: documents/models.py:1487
+#: documents/models.py:1491
msgid "grant change permissions to these users"
msgstr ""
-#: documents/models.py:1494
+#: documents/models.py:1498
msgid "grant change permissions to these groups"
msgstr ""
-#: documents/models.py:1501
+#: documents/models.py:1505
msgid "assign these custom fields"
msgstr ""
-#: documents/models.py:1505
+#: documents/models.py:1509
msgid "custom field values"
msgstr ""
-#: documents/models.py:1509
+#: documents/models.py:1513
msgid "Optional values to assign to the custom fields."
msgstr ""
-#: documents/models.py:1518
+#: documents/models.py:1522
msgid "remove these tag(s)"
msgstr ""
-#: documents/models.py:1523
+#: documents/models.py:1527
msgid "remove all tags"
msgstr ""
-#: documents/models.py:1530
+#: documents/models.py:1534
msgid "remove these document type(s)"
msgstr ""
-#: documents/models.py:1535
+#: documents/models.py:1539
msgid "remove all document types"
msgstr ""
-#: documents/models.py:1542
+#: documents/models.py:1546
msgid "remove these correspondent(s)"
msgstr ""
-#: documents/models.py:1547
+#: documents/models.py:1551
msgid "remove all correspondents"
msgstr ""
-#: documents/models.py:1554
+#: documents/models.py:1558
msgid "remove these storage path(s)"
msgstr ""
-#: documents/models.py:1559
+#: documents/models.py:1563
msgid "remove all storage paths"
msgstr ""
-#: documents/models.py:1566
+#: documents/models.py:1570
msgid "remove these owner(s)"
msgstr ""
-#: documents/models.py:1571
+#: documents/models.py:1575
msgid "remove all owners"
msgstr ""
-#: documents/models.py:1578
+#: documents/models.py:1582
msgid "remove view permissions for these users"
msgstr ""
-#: documents/models.py:1585
+#: documents/models.py:1589
msgid "remove view permissions for these groups"
msgstr ""
-#: documents/models.py:1592
+#: documents/models.py:1596
msgid "remove change permissions for these users"
msgstr ""
-#: documents/models.py:1599
+#: documents/models.py:1603
msgid "remove change permissions for these groups"
msgstr ""
-#: documents/models.py:1604
+#: documents/models.py:1608
msgid "remove all permissions"
msgstr ""
-#: documents/models.py:1611
+#: documents/models.py:1615
msgid "remove these custom fields"
msgstr ""
-#: documents/models.py:1616
+#: documents/models.py:1620
msgid "remove all custom fields"
msgstr ""
-#: documents/models.py:1625
+#: documents/models.py:1629
msgid "email"
msgstr ""
-#: documents/models.py:1634
+#: documents/models.py:1638
msgid "webhook"
msgstr ""
-#: documents/models.py:1638
+#: documents/models.py:1642
+msgid "passwords"
+msgstr ""
+
+#: documents/models.py:1646
+msgid ""
+"Passwords to try when removing PDF protection. Separate with commas or new "
+"lines."
+msgstr ""
+
+#: documents/models.py:1651
msgid "workflow action"
msgstr ""
-#: documents/models.py:1639
+#: documents/models.py:1652
msgid "workflow actions"
msgstr ""
-#: documents/models.py:1654
+#: documents/models.py:1667
msgid "triggers"
msgstr ""
-#: documents/models.py:1661
+#: documents/models.py:1674
msgid "actions"
msgstr ""
-#: documents/models.py:1664 paperless_mail/models.py:154
+#: documents/models.py:1677 paperless_mail/models.py:154
msgid "enabled"
msgstr ""
-#: documents/models.py:1675
+#: documents/models.py:1688
msgid "workflow"
msgstr ""
-#: documents/models.py:1679
+#: documents/models.py:1692
msgid "workflow trigger type"
msgstr ""
-#: documents/models.py:1693
+#: documents/models.py:1706
msgid "date run"
msgstr ""
-#: documents/models.py:1699
+#: documents/models.py:1712
msgid "workflow run"
msgstr ""
-#: documents/models.py:1700
+#: documents/models.py:1713
msgid "workflow runs"
msgstr ""