diff --git a/services/domain/Big.Application/ExpireRegistrationWorker.cs b/services/domain/Big.Application/ExpireRegistrationWorker.cs
new file mode 100644
index 0000000..6e900e7
--- /dev/null
+++ b/services/domain/Big.Application/ExpireRegistrationWorker.cs
@@ -0,0 +1,34 @@
+using Big.Domain;
+
+namespace Big.Application;
+
+///
+/// Handles one acquired RegistratieVerlopen external-worker job (S-10a, ADR-0017): load the
+/// registration the job correlates to and expire it to VERLOPEN — the 30-day document-wait timer fired
+/// before the documents arrived, so the case is cancelled. Pure application logic over ports; it knows
+/// nothing of Flowable. The polling loop that feeds it jobs lives in Infrastructure. Mirrors
+/// .
+///
+public sealed class ExpireRegistrationWorker(IRegistrationStore store)
+{
+ ///
+ /// Process the job. Idempotent: a redelivered job whose registration is already VERLOPEN is a
+ /// no-op — not persisted again (§8.6, at-least-once delivery). An unknown registration is an error:
+ /// it throws, leaving the job un-completed for Flowable to redeliver.
+ ///
+ public async Task HandleAsync(RegistratieVerlopenJob job, CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(job);
+
+ var registration = await store.GetAsync(job.RegistrationId, ct)
+ ?? throw new InvalidOperationException(
+ $"No registration {job.RegistrationId} for RegistratieVerlopen job {job.JobId}.");
+
+ // A redelivered job whose registration is already VERLOPEN completes without persisting again.
+ if (registration.Status == RegistrationStatus.Verlopen)
+ return;
+
+ registration.Expire();
+ await store.SaveAsync(registration, ct);
+ }
+}
diff --git a/services/domain/Big.Application/Ports.cs b/services/domain/Big.Application/Ports.cs
index b89ac82..492b232 100644
--- a/services/domain/Big.Application/Ports.cs
+++ b/services/domain/Big.Application/Ports.cs
@@ -95,3 +95,11 @@ public sealed record OpenZaakJob(string JobId, RegistrationId RegistrationId);
/// once the 14-day boundary timer fires (ADR-0015).
///
public sealed record EscalatieJob(string JobId, string ProcessInstanceId);
+
+///
+/// An acquired RegistratieVerlopen job (S-10a): the Flowable job id and the registration id it
+/// carries as a process variable. The 30-day boundary timer on WachtOpDocumenten spawns it when
+/// the required documents were not supplied in time; expiring the correlated registration to VERLOPEN
+/// cancels the case (ADR-0017).
+///
+public sealed record RegistratieVerlopenJob(string JobId, RegistrationId RegistrationId);
diff --git a/services/domain/Big.Infrastructure/IExternalWorkerClient.cs b/services/domain/Big.Infrastructure/IExternalWorkerClient.cs
index ade8872..912ff59 100644
--- a/services/domain/Big.Infrastructure/IExternalWorkerClient.cs
+++ b/services/domain/Big.Infrastructure/IExternalWorkerClient.cs
@@ -36,3 +36,19 @@ public interface IBeoordelingEscalatieClient
/// Complete an acquired escalation job so its token reaches the escalation end event.
Task CompleteBeoordelingEscalatieJobAsync(string jobId, CancellationToken ct = default);
}
+
+///
+/// The document-timeout side of the Workflow Client (S-10a): the RegistratieVerlopen
+/// external-worker jobs parked by the 30-day boundary timer on WachtOpDocumenten. Kept separate
+/// from the other worker ports (interface segregation) so neither the OpenZaak nor escalation worker
+/// sees expiry. Implemented by — the only code that talks to
+/// Flowable (§8.2, ADR-0017).
+///
+public interface IRegistratieVerlopenClient
+{
+ /// Acquire and lock up to RegistratieVerlopen jobs.
+ Task> AcquireRegistratieVerlopenJobsAsync(int maxJobs, CancellationToken ct = default);
+
+ /// Complete an acquired expiry job so its token reaches the endVerlopen end event.
+ Task CompleteRegistratieVerlopenJobAsync(string jobId, CancellationToken ct = default);
+}
diff --git a/services/domain/Big.Infrastructure/RegistratieVerlopenProcessor.cs b/services/domain/Big.Infrastructure/RegistratieVerlopenProcessor.cs
new file mode 100644
index 0000000..681e765
--- /dev/null
+++ b/services/domain/Big.Infrastructure/RegistratieVerlopenProcessor.cs
@@ -0,0 +1,41 @@
+using Big.Application;
+using Microsoft.Extensions.Logging;
+
+namespace Big.Infrastructure;
+
+///
+/// One poll tick of the document-timeout worker (S-10a, ADR-0017): acquire the parked
+/// RegistratieVerlopen jobs — the tokens the 30-day boundary timer on WachtOpDocumenten
+/// spawns — expire each correlated registration via the , and
+/// complete the job so its token reaches endVerlopen. A job that fails is logged and left
+/// un-completed so Flowable redelivers it (§8.6). Split out from the hosted pump so the
+/// acquire→expire→complete logic is unit-testable without a running host. Mirrors
+/// and .
+///
+public sealed class RegistratieVerlopenProcessor(
+ IRegistratieVerlopenClient client,
+ ExpireRegistrationWorker worker,
+ ILogger logger)
+{
+ /// Acquire and process up to jobs. Returns the number acquired.
+ public async Task PumpOnceAsync(int maxJobs, CancellationToken ct = default)
+ {
+ var jobs = await client.AcquireRegistratieVerlopenJobsAsync(maxJobs, ct);
+
+ foreach (var job in jobs)
+ {
+ try
+ {
+ await worker.HandleAsync(job, ct);
+ await client.CompleteRegistratieVerlopenJobAsync(job.JobId, ct);
+ }
+ catch (Exception ex)
+ {
+ // Leave the job un-completed: its lock expires and Flowable redelivers it (§8.6).
+ logger.LogError(ex, "RegistratieVerlopen job {JobId} failed; leaving it for redelivery.", job.JobId);
+ }
+ }
+
+ return jobs.Count;
+ }
+}