{"id":517,"date":"2026-05-21T15:33:14","date_gmt":"2026-05-21T13:33:14","guid":{"rendered":"https:\/\/www.rothamel.com\/?p=517"},"modified":"2026-05-21T15:33:14","modified_gmt":"2026-05-21T13:33:14","slug":"ai-based-document-processing-with-camunda-8","status":"publish","type":"post","link":"https:\/\/www.rothamel.com\/index.php\/2026\/05\/21\/ai-based-document-processing-with-camunda-8\/","title":{"rendered":"AI based document processing with Camunda 8"},"content":{"rendered":"\n<p>This tutorial guides you through setting up a <strong>local, privacy-compliant Intelligent Document Processing (IDP) pipeline<\/strong> using <strong>Camunda 8 (Desktop\/Run)<\/strong>, <strong>Ollama<\/strong> (for the LLM), and a <strong>Python External Task Worker<\/strong> (for OCR and file operations).<\/p>\n\n\n\n<p>The pipeline reads PDF documents using OCR, analyzes the content with an Agentic AI (local LLM) to determine the sender and document type, and automatically reorganizes and renames the files within your local file system.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture Overview<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91; Input Folder ] \u2794 (Python Worker: OCR) \u2794 &#91; Camunda 8 Engine ]\n                                               \u2502\n &#91; Local Directory ] \u2b8c (Python Worker) \u2b80 &#91; AI Agent Connector (Ollama) ]\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. Prerequisites &amp; Local Infrastructure<\/h2>\n\n\n\n<h2 class=\"wp-block-heading\">Install Ollama &amp; LLM<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Download and install Ollama.<\/li>\n\n\n\n<li>Open your terminal and download an LLM well-suited for structured JSON output and tool calling (e.g., <code>Llama 3<\/code> or <code>Mistral<\/code>):<code>ollama run llama3 <\/code><em>Note: Ollama exposes an OpenAI-compatible API endpoint by default at <code>http:\/\/localhost:11434\/v1<\/code>.<\/em><\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Set Up Python Environment<\/h2>\n\n\n\n<p>Create a project directory and install the required dependencies, including the official <a href=\"https:\/\/docs.camunda.io\/docs\/apis-tools\/python-sdk\/\">Camunda 8 Python SDK<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pip install camunda-orchestration-sdk pypdf pytesseract pdf2image OpenAI\n<\/code><\/pre>\n\n\n\n<p><em>(Ensure that Tesseract OCR is installed on your system and added to your system environment variables\/PATH).<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. Camunda BPMN Process Modeling<\/h2>\n\n\n\n<p>Create a new BPMN diagram in the <strong>Camunda Desktop Modeler<\/strong> or <strong>Web Modeler<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Process Steps Definition<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Service Task 1<\/strong>: &#8220;Extract Document Text via OCR&#8221;\n<ul class=\"wp-block-list\">\n<li><strong>Type (Job Worker Key)<\/strong>: <code>ocr-extract<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>AI Agent Task (Connector)<\/strong>: &#8220;Analyze Document Content&#8221;\n<ul class=\"wp-block-list\">\n<li>Use the <strong>AI Agent Connector<\/strong> available in Camunda 8.<\/li>\n\n\n\n<li><strong>Configuration<\/strong>:\n<ul class=\"wp-block-list\">\n<li><strong>Provider<\/strong>: <code>OpenAI-compatible<\/code> (or Custom REST depending on your connector version)<\/li>\n\n\n\n<li><strong>Endpoint<\/strong>: <code>http:\/\/localhost:11434\/v1<\/code><\/li>\n\n\n\n<li><strong>Model ID<\/strong>: <code>llama3<\/code><\/li>\n\n\n\n<li><strong>System Prompt<\/strong>:<code>You are a precise document classifier. Analyze the text and extract the sender (company or person) and the document type (e.g., Invoice, Contract, Cancellation, Cover Letter). Respond exclusively in valid JSON format: {\"sender\": \"Name\", \"type\": \"Type\"}.<\/code><\/li>\n\n\n\n<li><strong>Prompt<\/strong>: <code>Context Text: {{ocr_text}}<\/code><\/li>\n\n\n\n<li><strong>Result Variable<\/strong>: <code>analysis_result<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Service Task 3<\/strong>: &#8220;Move and Rename File&#8221;\n<ul class=\"wp-block-list\">\n<li><strong>Type (Job Worker Key)<\/strong>: <code>file-organizer<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. Implementing the Python Job Worker<\/h2>\n\n\n\n<p>Create a file named <code>app.py<\/code>. This worker handles the extraction of the PDF content and the final local file system operations.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import os\nimport json\nimport shutil\nfrom pathlib import Path\nfrom pypdf import PdfReader\nimport pytesseract\nfrom pdf2image import convert_from_path\nfrom camunda.client import CamundaClient\n\n<em># Configuration<\/em>\nINPUT_DIR = Path(\".\/input\")\nSTORAGE_ROOT = Path(\".\/archive\")\n\n<em># Initialize local directory structures<\/em>\nINPUT_DIR.mkdir(exist_ok=True)\nSTORAGE_ROOT.mkdir(exist_ok=True)\n\n<em># Initialize Camunda Client (Default configurations for Self-Managed \/ Local Run)<\/em>\nclient = CamundaClient() \n\ndef extract_text_from_pdf(pdf_path):\n    \"\"\"Extracts native text, falls back to OCR if the PDF is a scan.\"\"\"\n    text = \"\"\n    reader = PdfReader(pdf_path)\n    for page in reader.pages:\n        text += page.extract_text() or \"\"\n    \n    <em># Fallback to OCR if no native text is found<\/em>\n    if not text.strip():\n        images = convert_from_path(pdf_path)\n        for img in images:\n            text += pytesseract.image_to_string(img)\n            \n    return text\n\n<em># 1. WORKER: OCR &amp; Text Extraction<\/em>\n@client.worker(type=\"ocr-extract\")\ndef handle_ocr(job):\n    file_name = job.variables.get(\"file_name\")\n    file_path = INPUT_DIR \/ file_name\n    \n    if not file_path.exists():\n        return {\"error\": \"File not found\"}\n        \n    extracted_text = extract_text_from_pdf(file_path)\n    \n    <em># Restrict context length to fit local LLM context windows smoothly<\/em>\n    return {\"ocr_text\": extracted_text&#91;:4000]} \n\n<em># 2. WORKER: File Organization &amp; Renaming<\/em>\n@client.worker(type=\"file-organizer\")\ndef handle_organization(job):\n    file_name = job.variables.get(\"file_name\")\n    analysis_raw = job.variables.get(\"analysis_result\")\n    \n    <em># Parse the JSON string returned by the LLM<\/em>\n    try:\n        data = json.loads(analysis_raw)\n        sender = data.get(\"sender\", \"Unknown\").replace(\" \", \"_\")\n        doc_type = data.get(\"type\", \"Other\").replace(\" \", \"_\")\n    except Exception:\n        sender, doc_type = \"Unknown\", \"Unclassified\"\n\n    <em># Define target directory structure (e.g., .\/archive\/Acme_Corp\/Invoice\/)<\/em>\n    target_dir = STORAGE_ROOT \/ sender \/ doc_type\n    target_dir.mkdir(parents=True, exist_ok=True)\n    \n    <em># Generate new standardized filename (e.g., Invoice_Acme_Corp.pdf)<\/em>\n    file_extension = Path(file_name).suffix\n    new_file_name = f\"{doc_type}_{sender}{file_extension}\"\n    target_path = target_dir \/ new_file_name\n    \n    <em># Execute file movement<\/em>\n    source_path = INPUT_DIR \/ file_name\n    shutil.move(str(source_path), str(target_path))\n    \n    print(f\"File successfully archived: {target_path}\")\n    return {\"status\": \"success\", \"archived_path\": str(target_path)}\n\nif __name__ == \"__main__\":\n    print(\"Python Camunda Workers active. Awaiting jobs...\")\n    client.start_workers()\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. Triggering and Testing the Pipeline<\/h2>\n\n\n\n<p>To feed documents into the system, you can use a secondary helper script that scans your input directory and instantiates a process instance in Camunda for each discovered file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># trigger_process.py<\/em>\nimport os\nfrom camunda.client import CamundaClient\n\nclient = CamundaClient()\n\ndef check_for_new_files():\n    for file in os.listdir(\".\/input\"):\n        if file.endswith(\".pdf\"):\n            <em># Pass the filename into the process variables<\/em>\n            client.start_process_instance(\n                process_definition_key=\"Process_Document_Pipeline\", \n                variables={\"file_name\": file}\n            )\n            print(f\"Process instance started for file: {file}\")\n\nif __name__ == \"__main__\":\n    check_for_new_files()\n<\/code><\/pre>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Place a sample PDF document inside the <code>.\/input<\/code> folder.<\/li>\n\n\n\n<li>Run <code>python app.py<\/code> in your terminal to keep your workers listening.<\/li>\n\n\n\n<li>In a separate terminal window, run <code>python trigger_process.py<\/code>.<\/li>\n\n\n\n<li>Your file will process through Camunda, get parsed by <strong>Ollama<\/strong>, and land perfectly organized under <code>.\/archive\/[Sender]\/[Type]\/<\/code>.<\/li>\n<\/ol>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This tutorial guides you through setting up a local, privacy-compliant Intelligent Document Processing (IDP) pipeline using Camunda 8 (Desktop\/Run), Ollama (for the LLM), and a Python External Task Worker (for OCR and file operations). The pipeline reads PDF documents using OCR, analyzes the content with an Agentic AI (local LLM) to determine the sender and [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"class_list":["post-517","post","type-post","status-publish","format-standard","hentry","category-java"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/posts\/517","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/comments?post=517"}],"version-history":[{"count":1,"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/posts\/517\/revisions"}],"predecessor-version":[{"id":518,"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/posts\/517\/revisions\/518"}],"wp:attachment":[{"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/media?parent=517"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/categories?post=517"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.rothamel.com\/index.php\/wp-json\/wp\/v2\/tags?post=517"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}