<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Derek Jackson</title>
    <description>The latest articles on DEV Community by Derek Jackson (@derek_jackson_0507a460026).</description>
    <link>https://dev.to/derek_jackson_0507a460026</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071884%2F594d47c6-5075-43be-bc86-157e0c024e5d.png</url>
      <title>DEV Community: Derek Jackson</title>
      <link>https://dev.to/derek_jackson_0507a460026</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/derek_jackson_0507a460026"/>
    <language>en</language>
    <item>
      <title>I Built an Automated Insurance Claims Processor on AWS. Terraform Broke It in an Interesting Way.</title>
      <dc:creator>Derek Jackson</dc:creator>
      <pubDate>Tue, 01 Sep 2026 18:08:31 +0000</pubDate>
      <link>https://dev.to/derek_jackson_0507a460026/i-built-an-automated-insurance-claims-processor-on-aws-terraform-broke-it-in-an-interesting-way-47j5</link>
      <guid>https://dev.to/derek_jackson_0507a460026/i-built-an-automated-insurance-claims-processor-on-aws-terraform-broke-it-in-an-interesting-way-47j5</guid>
      <description>&lt;p&gt;I built an automated insurance claims document processor on AWS. A claim PDF is uploaded and you get back a clean, structured database record with no manual data entry. Then I rebuilt the whole system in Terraform. The rebuild caused tricky bugs at several stages. This post walks through those bugs in detail. Each bug followed the same pattern: everything looks fine, nothing works, and no error appears anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As someone with a background in insurance operations, I wanted at least one of my AWS portfolio projects to reflect that. I chose a build that bridges my operational expertise with Fintech, creating a natural translation of my real-world domain knowledge into the cloud ecosystem. &lt;/p&gt;

&lt;p&gt;The pipeline processes a Long-Term Disability (LTD) claim form. This form is modeled on the structure of a real LTD claim and extracts an allowlist of the 14 fields necessary for claims processing: policy number, date last worked, coverage start date, etc. &lt;/p&gt;

&lt;p&gt;I decided to build it twice. First manually through the AWS Console, to visualize each service and their relationship with one another. Then I rebuilt the entire thing as IaC using Terraform, so the infrastructure is fully reproducible. I split the code into modules for compute, database ,iam, messaging and storage, and used the rebuild to add an SQS queue and DLQ that the manual build did not have. That second pass is where things got interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvd13zcntn9iw4ufjbck3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvd13zcntn9iw4ufjbck3.png" alt=" " width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A claim form is uploaded s3://bucket/incoming/. The upload triggers Lambda #1. Lambda #1 starts an asynchronous Textract job and returns immediately.&lt;/p&gt;

&lt;p&gt;Why asynchronous? This is a multi-page document. Textract's synchronous API only handles single-page documents and it's bound by Lambda's execution timeout. So an asynchronous job was the only option.&lt;/p&gt;

&lt;p&gt;Textract reads the S3 object. When the job finishes, Textract publishes a completion message to an SNS topic. SNS delivers this message to an SQS queue. A dead-letter queue (DLQ) sits behind this SQS queue to catch failed messages.&lt;/p&gt;

&lt;p&gt;The SQS layer adds automatic retries (up to 3 attempts), and a safety net for messages that fail permanently. Without the DLQ, an unread form would be lost.&lt;/p&gt;

&lt;p&gt;Once the message is delivered to SQS, it triggers Lambda #2. Lambda #2 retrieves the full Textract results, filters the results down to the 14-field allowlist and writes an item to DynamoDB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rebuilding in Terraform: three hiccups&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Console-built version worked without any of the problems below. Each problem I encountered was a result of the additions I made in the Terraform version. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problems and Fixes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. S3 bucket name collision&lt;/strong&gt;&lt;br&gt;
Although my original bucket was destroyed during the teardown, the S3 namespace can take time to release a deleted name. This triggering a temporary collision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**Error**: creating S3 Bucket (derek-jackson-claims-processor-2026): BucketAlreadyExists

  with module.storage.aws_s3_bucket.claims,
  on modules/storage/main.tf line 1, in resource "aws_s3_bucket" "claims":
   1: resource "aws_s3_bucket" "claims" {
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: I removed the hardcoded bucket name and generated a random suffix with Terraform's random_id resource instead. A small tweak, but a permanent fix for any future rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A three-stage KMS permission chain&lt;/strong&gt;&lt;br&gt;
The console version used AWS's default encryption everywhere. When I did the Terraform build, I decided to use a Customer Managed Key (CMK) — rather than AWS's default managed keys — for S3, DynamoDB, SNS, and SQS, in order to control exactly which services and roles could use the key. Compliance-driven environments commonly require CMKs for this reason. Although I used a single key for simplicity, in a live environment, the principle of least privilege would dictate using isolated keys per resource to minimize the blast radius.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The lesson here&lt;/strong&gt;: a KMS key's own policy is a separate trust boundary from IAM, which wasn't obvious to me until I hit it. You can grant a Lambda's IAM role &lt;code&gt;kms:Decrypt&lt;/code&gt; on the key. But that grant does not let another AWS service, acting on your behalf, use that key. The key's own policy must name that service directly because, by default, a CMK only trusts standard users and roles within your account and automatically blocks automated AWS services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The pipeline touches three separate points where a service other than a Lambda role needs to work with KMS-encrypted data, and each failure was completely silent; nothing threw a visible error, things just stopped working one step further down the chain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Textract → S3 input (read)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bug&lt;/strong&gt;: Textract's async jobs run in Textract's own backend using it's own service identity, not the Lambda's temporary credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix&lt;/strong&gt;: I granted &lt;code&gt;textract.amazonaws.com&lt;/code&gt; the &lt;code&gt;kms:Decrypt&lt;/code&gt; permission directly in the CMK's key policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Textract → SNS topic (publish)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bug&lt;/strong&gt;: I confirmed via the CLI that the Textract job itself was succeeded — &lt;code&gt;JobStatus: SUCCEEDED&lt;/code&gt; — but nothing was reaching SQS. The dedicated &lt;code&gt;textract-sns-role&lt;/code&gt; had &lt;code&gt;sns:Publish&lt;/code&gt; permission but no KMS permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix&lt;/strong&gt;: I added &lt;code&gt;kms:GenerateDataKey&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; to &lt;code&gt;textract-sns-role&lt;/code&gt;'s IAM policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SNS → SQS queue (delivery)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bug&lt;/strong&gt;: this is a documented AWS architectural requirement, not something I found by trial and error. The SNS service principal itself needs an explicit KMS grant to deliver messages into an encrypted SQS queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix&lt;/strong&gt;: I granted &lt;code&gt;sns.amazonaws.com&lt;/code&gt; the same two KMS actions in the key's policy. After applying this fix, a message finally reached the dead-letter queue! Showing progress, and confirming the full Textract → SNS → SQS chain was finally delivering messages. It landed in the DLQ rather than succeeding outright because Lambda #2 still had a separate bug which I addressed next.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The lesson here&lt;/strong&gt;: whenever Service A hands data to Service B, and B's resource is encrypted with a customer managed key, Service A needs its own grant on that key. Being able to call the API (&lt;code&gt;sns:Publish&lt;/code&gt;, &lt;code&gt;sqs:SendMessage&lt;/code&gt;) and being able to use the KMS key the resource is encrypted with are separate permissions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. SQS vs. SNS event shape&lt;/strong&gt;&lt;br&gt;
With the KMS chain resolved, a message finally reached SQS, but Lambda #2 was still failing. Fortunately, now with a visible error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Sns&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="nc"&gt;Traceback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/task/lambda_function.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lambda_handler&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambda #2's code assumed a direct SNS trigger. But this architecture routes through SQS and since the SNS→SQS subscription doesn't have raw message delivery enabled, SQS wraps the entire SNS envelope as a JSON string inside its own &lt;code&gt;body&lt;/code&gt; field leaving two layers to unwrap, not one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sns_envelope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sns_envelope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a good example of an implicit contract bug: two pieces of code each made a reasonable assumption independently, and those assumptions quietly contradicted each other.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging method&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I verified each hop in the chain on its own, starting closest to the source:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Queried Textract's job status directly via the CLI. This step bypasses the entire pipeline, and confirmed the Textract job itself succeeded, ruling that stage out and pointing to a problem in the message delivery.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Checked SQS and DLQ message counts. This step confirms whether a message ever reached SQS. It showed no message arriving. This pointed to a delivery failure between SNS and SQS, caused by the missing KMS grants.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After adding the missing grants, I checked CloudWatch logs which surfaced the &lt;code&gt;KeyError: 'Sns'&lt;/code&gt; error. This error pointed to the SQS/SNS event-shape mismatch in Lambda #2.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;4.Fixed the event-shape mismatch by unwrapping the SQS body before the SNS message inside it. This change let Lambda #2 correctly parse the message. It processed the claim data without error. The pipeline then ran end to end, from upload to a clean DynamoDB record, with no further issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proof it works&lt;/strong&gt;&lt;br&gt;
The original console-built pipeline, tested against a real-structured LTD claim form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjpr89nrohll6zjcosk5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjpr89nrohll6zjcosk5o.png" alt=" " width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxzg37hqgdnopnvgdvs5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxzg37hqgdnopnvgdvs5.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rebuilt Terraform pipeline, tested end-to-end against a second mock document with intentionally obvious fake data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuca4dwc5kfl4d2yn9vbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuca4dwc5kfl4d2yn9vbu.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DynamoDB item from the Terraform rebuild, part 1 — claim_id, city: Sampleville, coverage_start_date, date_last_worked, date_of_birth, earnings, employer_contact_name, employer_name: Acme Test Corp&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmmkl6n8qz6dcgz3pjag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmmkl6n8qz6dcgz3pjag.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DynamoDB item from the Terraform rebuild, part 2 — full_name: John Q. Testperson, job_title, policy_number: TEST-000000, social_security_number: 123-45-6789, state, street_address, zip_code: 00000&lt;/p&gt;

&lt;p&gt;And the dead-letter queue moment that confirmed the KMS chain was finally resolved — the first message to make it all the way from Textract through SNS to SQS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssvjfzrrx1sdgo2fo6qi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssvjfzrrx1sdgo2fo6qi.png" alt=" " width="799" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SQS console showing the callback queue at 0 messages and the dead-letter queue with 1 message available&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd do next time: AWS Lambda durable functions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS released Lambda durable functions in December 2025. This feature fits the "start a task, wait for a callback" pattern used in this project.&lt;/p&gt;

&lt;p&gt;Using durable functions would merge Lambda #1 and Lambda #2 into a single function which would use a waitForCallback() operation. This change would remove the SQS/DLQ layer, because durable functions would handle retry and durability themselves.&lt;/p&gt;

&lt;p&gt;A thin relay lambda would still be necessary since Textract can't call the durable execution callback API directly. The relay function would translate the SNS notification into that call.&lt;/p&gt;

&lt;p&gt;The tradeoff with this approach is the SQS/DLQ layer is no longer needed, which also removes the need for the SNS to SQS KMS grant. This simplified infrastructure would only require a relay function rather than a queue-based messaging layer with its own encryption requirements.&lt;/p&gt;

&lt;p&gt;Code and Terraform modules for this project are on my GitHub: &lt;a href="https://github.com/DIJSAA/claims-processor-terraform" rel="noopener noreferrer"&gt;https://github.com/DIJSAA/claims-processor-terraform&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>serverless</category>
      <category>textract</category>
    </item>
  </channel>
</rss>
