AutomationFlowsAI & RAG › Generate Freelance Proposal Pdfs From Tally with Gpt-4o and Gmail

Generate Freelance Proposal Pdfs From Tally with Gpt-4o and Gmail

ByRuth Aju @ruthbuilds on n8n.io

This workflow generates a personalized freelance proposal from a Tally form submission, using OpenAI to extract text from screenshots and write the proposal, SerpAPI to pull proposal examples, Google Sheets to reference past projects, pdf.co to create a PDF, and Gmail to email…

Event trigger★★★★☆ complexityAI-powered19 nodesGoogle Sheets ToolN8N Nodes TallyformsOpenAIN8N Nodes SerpapiAgentOpenAI ChatHTTP RequestGmail
AI & RAG Trigger: Event Nodes: 19 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #16170 — we link there as the canonical source.

This workflow follows the Agent → Gmail recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "1SJIFmQ0t04plzVE",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Freelancers Proposal Generator (Screenshot or Text)",
  "tags": [],
  "nodes": [
    {
      "id": "3c7bbf2a-9b5f-41c4-8819-2630b81400ae",
      "name": "get_relevant_projects",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        1456,
        304
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1y9h93YqTwPMUC8lP61C7ZAxKHA9UpNLkomrGGuB6h58/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1y9h93YqTwPMUC8lP61C7ZAxKHA9UpNLkomrGGuB6h58",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1y9h93YqTwPMUC8lP61C7ZAxKHA9UpNLkomrGGuB6h58/edit?usp=drivesdk",
          "cachedResultName": "Project Data"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "cb8c7af0-0d23-4519-9107-ddfa5ca88b36",
      "name": "Format text - text",
      "type": "n8n-nodes-base.set",
      "position": [
        -416,
        128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5bf738fa-d1da-4fe9-923e-9829fc4c15d3",
              "name": "Job title",
              "type": "string",
              "value": "={{ $('Form Trigger').item.json.question_52y7Lv.value }}"
            },
            {
              "id": "1d348046-9a89-4f26-b491-2837ab3c7ff0",
              "name": "Job description",
              "type": "string",
              "value": "={{ $json['0'].content[0].text }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "6dc3ffc4-b7d2-4bc3-abbc-29675018434e",
      "name": "Format text - image",
      "type": "n8n-nodes-base.set",
      "position": [
        -48,
        -176
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5bf738fa-d1da-4fe9-923e-9829fc4c15d3",
              "name": "Job title",
              "type": "string",
              "value": "={{ $('Form Trigger').item.json.question_52y7Lv.value }}"
            },
            {
              "id": "1d348046-9a89-4f26-b491-2837ab3c7ff0",
              "name": "Job description",
              "type": "string",
              "value": "={{ $json['0'].content[0].text }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a7c6e656-01ba-49bf-be04-9f234468ba09",
      "name": "Form Trigger",
      "type": "n8n-nodes-tallyforms.tallyTrigger",
      "position": [
        -1120,
        0
      ],
      "parameters": {
        "formId": "J9oJ9K"
      },
      "credentials": {
        "tallyApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "e83ee2a0-abc6-4a4a-8029-a1d99f563dde",
      "name": "If - Image or Text?",
      "type": "n8n-nodes-base.if",
      "position": [
        -784,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8d14a3cc-40ad-4d28-882d-b8d5826e360d",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              },
              "leftValue": "={{ $json.question_dP5MlD.value }}",
              "rightValue": "[null]"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "c66ff3d4-9b7b-45be-b66b-40e75edd8350",
      "name": "Extract Text from Image",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -528,
        -176
      ],
      "parameters": {
        "text": "Paste the text from the image.",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "GPT-4O"
        },
        "options": {},
        "resource": "image",
        "imageUrls": "={{ $json.question_YdL6rz.value }}",
        "operation": "analyze"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "e996f904-bbde-4571-8f77-32d6858f4aa1",
      "name": "Search Proposal Examples",
      "type": "n8n-nodes-serpapi.serpApi",
      "position": [
        416,
        128
      ],
      "parameters": {
        "q": "=how to write freelance proposal for {{ $json['Job Title'] }} example",
        "requestOptions": {},
        "additionalFields": {
          "num": "5"
        }
      },
      "credentials": {
        "serpApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ecb7b413-b8a6-4dc3-9f32-c551e7e90147",
      "name": "Build AI Input",
      "type": "n8n-nodes-base.code",
      "position": [
        816,
        128
      ],
      "parameters": {
        "jsCode": "const results = $input.all()[0].json.organic_results || [];\n\nconst snippets = results\n  .filter(r => r.snippet && r.snippet.length > 50)\n  .filter(r => !r.snippet.toLowerCase().includes('belikenative') &&\n               !r.snippet.toLowerCase().includes('localization') &&\n               !r.snippet.toLowerCase().includes('job post generator'))\n  .slice(0, 5)\n  .map(r => `- ${r.snippet}`)\n  .join('\\n');\n\nlet jobTitle = '';\nlet jobDescription = '';\n\ntry {\n  jobTitle = $('Format text - image').first().json['Job title'] || '';\n  jobDescription = $('Format text - image').first().json['Job description'] || '';\n} catch(e) {}\n\nif (!jobTitle) {\n  try {\n    jobTitle = $('Format text - text').first().json['Job title'] || '';\n    jobDescription = $('Format text - text').first().json['Job description'] || '';\n  } catch(e) {}\n}\n\nreturn [{\n  json: {\n    proposalSamples: snippets || 'No samples found',\n    jobDescription,\n    jobTitle\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "61a367f5-ff1c-484b-984c-b423eaf24deb",
      "name": "Generate Proposal",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1264,
        128
      ],
      "parameters": {
        "text": "=Job title: {{ $json.jobTitle }}\n\nJob description: {{ $json.jobDescription }}\n\nProposal samples: {{ $json.proposalSamples }}\n\nDate and Time: {{ $now }}",
        "options": {
          "systemMessage": "=You are a professional freelance proposal writer working on behalf of the freelancer described in the owner bio below.\n\nYou will receive a job title, a job description (typed or extracted from a screenshot), and sample proposal snippets from the web for structural reference.\n\nBefore writing anything, call the get_relevant_projects tool using the job title and a short summary of the job description. Read what comes back and identify the most relevant projects. If nothing useful comes back, skip the project reference section entirely and write a strong proposal based on the web samples and your own judgment. Do not write the proposal before calling the tool.\n\nWrite in first person throughout. Use \"I\", \"my\", \"I've\", \"I built\". Never refer to the freelancer by name or in third person.\n\nNever use em dashes anywhere in the proposal.\n\nNever use these words: seamlessly, leverage, leveraging, impactful, transforms, cutting-edge, innovative, dynamic, tailor, tailored, passionate, delve, bottleneck, game-changer, results-driven, spearhead, synergy, utilize, robust, scalable, streamline, streamlined.\n\nDo not open with a generic statement about the industry or automation. Open directly with the client's specific pain point. Show you read their posting carefully. Do not start the proposal with \"I\" as the very first word. Do not open with \"Hi\" or \"Hello\".\n\nThe proposal should read as one flowing piece of writing. No bullet points, numbered lists, headers, or section labels anywhere.\n\nMild imperfection is fine. It should sound like a real person wrote it.\n\nKeep the proposal between 180 and 260 words. No shorter, no longer.\n\nDo not repeat the same point in different words. Do not over-explain.\n\nFollow this structure in order:\n\nOpen by naming the client's core problem in plain language. One paragraph.\n\nIntroduce yourself in one to two sentences. Keep it short. This is context, not a pitch.\n\nIf get_relevant_projects returned something useful, mention one relevant project naturally in one or two sentences. Sound like you are casually bringing it up, not showing off. If nothing relevant came back, skip this entirely. Do not make up experience.\n\nExplain specifically how you would solve their problem. Be concrete and pull actual details from their job description. Do not be vague.\n\nEnd with a simple, low pressure call to action. Invite them to ask questions or have a quick chat. Nothing pushy.\n\nDo not hallucinate skills, credentials, or experience that are not in the owner bio or tool results. Do not mention the get_relevant_projects tool or that you searched anything. Do not mention the web samples. Do not add a subject line, email header, or signature block. Do not end with a name. Do not start any sentence with \"Lastly\", \"Furthermore\", \"Moreover\", \"In conclusion\", or \"To summarize\".\n\nIf the job description is too vague to write a specific proposal, return exactly this inside the proposal field: \"Job description is too vague to generate a strong proposal. Please add more details and try again.\" Do not write anything else.\n\n\n### OWNER BIO:\n\n[Replace this with your own name, role, primary tools, and 3 to 5 past projects. For each project include a brief description of the problem, your approach, and the result.]\n\n\n### OUTPUT FORMAT:\n\nReturn your response as valid JSON with exactly two fields. No preamble, no explanation, no markdown backticks, no text outside the JSON:\n\n{\n  \"proposal\": \"<full proposal in HTML format. Use <p> tags only to wrap each paragraph. No <h1>, <h2>, <ul>, <ol>, <li>, or any other tags. Just clean <p> tags. Each paragraph is one <p> block.>\",\n  \"emailBody\": \"<short 2 to 3 sentence email body in HTML format. Tell the recipient their proposal is ready, it is attached as a PDF, and they can reach out with any questions. Warm but brief. Use <p> tags only.>\"\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "9f531967-3656-4bfb-b62a-b08535d25350",
      "name": "GPT-4o",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1216,
        304
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "34dcaef5-aaf8-448f-93e9-82743710e962",
      "name": "Parse AI Output",
      "type": "n8n-nodes-base.code",
      "position": [
        1808,
        128
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json.output;\n\nlet parsed;\ntry {\n  parsed = JSON.parse(raw);\n} catch(e) {\n  parsed = {\n    proposal: '<p>Error generating proposal.</p>',\n    emailBody: '<p>Your proposal is ready. Please see attached PDF.</p>'\n  };\n}\n\nreturn [{\n  json: {\n    proposal: parsed.proposal,\n    emailBody: parsed.emailBody\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4158d36e-27bf-47d0-964c-5e194e342b1d",
      "name": "Convert to PDF",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2144,
        128
      ],
      "parameters": {
        "url": "https://api.pdf.co/v1/pdf/convert/from/html",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"html\": \"{{ $json.proposal }}\",\n  \"name\": \"proposal.pdf\",\n  \"async\": false\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "7ed43b64-1f1d-458a-ab60-bf6cf6d58dfb",
      "name": "Download PDF",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2480,
        128
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {}
      },
      "typeVersion": 4.4
    },
    {
      "id": "b358b089-20eb-4820-8faf-a67c38eafe4f",
      "name": "Email Proposal",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2848,
        128
      ],
      "parameters": {
        "sendTo": "={{ $('Form Trigger').item.json.question_zea9ZZ.value }}",
        "message": "={{ $('Parse AI Output').item.json.emailBody }}",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          },
          "appendAttribution": false
        },
        "subject": "Your Proposal is ready!"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "58a1f418-79eb-4901-9a27-cbb538d4434d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2096,
        -256
      ],
      "parameters": {
        "width": 736,
        "height": 848,
        "content": "## Try It Out!\n\nThis n8n template lets you generate a personalized freelance proposal from any job description (typed or screenshotted) and delivers it to your inbox as a PDF in minutes.\n\nWhether you're applying for automation projects, AI builds, or any freelance role, this workflow handles the research and writing so you can focus on sending.\n\n## How it works\n- The job title and description come in through a Tally form. If you uploaded a screenshot instead of typing, GPT-4o pulls the text out of the image first.\n- SerpAPI searches Google for real proposal examples based on the job title to give the AI useful reference material.\n- The AI Agent checks a Google Sheet of your past projects and picks the most relevant ones to reference.\n- It then writes a first-person proposal and a short email body, both returned as HTML.\n- The proposal is converted to a PDF via pdf.co and emailed directly to you as an attachment.\n\n## How to set up\n- Tally: Build a form with fields for Job Title, Job Description, Job Screenshot, and Email. Drop your Form ID into the Tally Trigger node.\n- OpenAI: Connect your API credentials to the Extract Text and Generate Proposal nodes.\n- SerpAPI: Sign up at serpapi.com and connect your account.\n- Google Sheets: Set up a sheet with columns: Project Name, Problem, Approach, Result, Keywords. Fill it with your past work.\n- pdf.co: Grab your API key and add it as an HTTP Header Auth credential named \"PDF.co Header\".\n- Gmail: Connect your Gmail OAuth2 credential to the Email Proposal node.\n\n## Customize it\nOpen the AI Agent node and find the Owner Bio section inside the system prompt. Replace it with your own name, role, tools, and experience. That's what shapes the proposal voice. Don't skip this step.\n\n## Requirements\n- Tally account\n- OpenAI API key (GPT-4o)\n- SerpAPI account\n- Google Sheets with your past projects\n- pdf.co account\n- Gmail OAuth2"
      },
      "typeVersion": 1
    },
    {
      "id": "00bbf2ad-f468-4819-adc3-c7699438ea73",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1152,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 1248,
        "height": 576,
        "content": "## 1. Input Handling\nThis routes the submission based on input type. Image uploads go through GPT-4o for text extraction. Both branches output the same Job Title and Job Description fields."
      },
      "typeVersion": 1
    },
    {
      "id": "97f1873b-2484-443b-9ee3-007a1182c332",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        304,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 592,
        "content": "## 2. Research\nThis searches Google for proposal writing examples using the job title, filters the results, and combines them with the job details into one clean input for the AI."
      },
      "typeVersion": 1
    },
    {
      "id": "56ca06ee-4606-4574-9df7-bd0936ad2a99",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1136,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 784,
        "content": "## 3. AI Proposal Generation\nThe agent calls Get Past Projects first, then writes a first-person proposal and a short email body. Both outputs are returned as HTML in a single JSON response."
      },
      "typeVersion": 1
    },
    {
      "id": "056fb8c7-16ff-45d9-a8e7-f7cf36d3d69d",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1792,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 1264,
        "height": 672,
        "content": "## 4. Delivery\nParses the AI output, converts the proposal HTML to a PDF via pdf.co, downloads the file, and emails it with the generated email body to the address from the form."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "1be81c79-cf0a-4ca4-bb2d-c336cc784e63",
  "connections": {
    "GPT-4o": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Proposal",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Download PDF": {
      "main": [
        [
          {
            "node": "Email Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Form Trigger": {
      "main": [
        [
          {
            "node": "If - Image or Text?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI Input": {
      "main": [
        [
          {
            "node": "Generate Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to PDF": {
      "main": [
        [
          {
            "node": "Download PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Output": {
      "main": [
        [
          {
            "node": "Convert to PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Proposal": {
      "main": [
        [
          {
            "node": "Parse AI Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format text - text": {
      "main": [
        [
          {
            "node": "Search Proposal Examples",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format text - image": {
      "main": [
        [
          {
            "node": "Search Proposal Examples",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If - Image or Text?": {
      "main": [
        [
          {
            "node": "Extract Text from Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format text - text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_relevant_projects": {
      "ai_tool": [
        [
          {
            "node": "Generate Proposal",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Extract Text from Image": {
      "main": [
        [
          {
            "node": "Format text - image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Proposal Examples": {
      "main": [
        [
          {
            "node": "Build AI Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow generates a personalized freelance proposal from a Tally form submission, using OpenAI to extract text from screenshots and write the proposal, SerpAPI to pull proposal examples, Google Sheets to reference past projects, pdf.co to create a PDF, and Gmail to email…

Source: https://n8n.io/workflows/16170/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Inbox Guardian. Uses gmailTrigger, lmChatOpenAi, agent, textClassifier. Event-driven trigger; 66 nodes.

Gmail Trigger, OpenAI Chat, Agent +12
AI & RAG

Transform your salon/service business with this streamlined WhatsApp automation system featuring Claude integration, zero-setup database management, and intelligent conversation handling. Claude MCP I

WhatsApp Trigger, WhatsApp, Redis +11
AI & RAG

Automate your personal productivity with this intelligent n8n workflow that integrates Telegram, Google Sheets, and OpenAI (GPT-4o). This system uses multiple AI agents to manage work hours, tasks, fi

Agent, OpenAI Chat, Telegram +9
AI & RAG

This automation is designed to help you generate AI-powered music tracks, cover art, and fully rendered music videos — all triggered from a simple Telegram chat and managed via Google Sheets.

OpenAI Chat, Memory Buffer Window, Output Parser Structured +11
AI & RAG

This n8n workflow creates an intelligent WhatsApp customer support bot that can handle text, image, audio, and document messages. The workflow automatically processes incoming messages through differe

HTTP Request, N8N Nodes Rapiwa, Agent Tool +9