Skip to main content

How to build an AI-powered campaign report generator for agency clients in Formaloo

Learn how to build an AI-powered campaign report generator in Formaloo. Log spend and results once, and turn them into a branded client PDF automatically.

What you'll build

By the end of this guide, you'll have a single campaign reporting form where your account managers log each client's spend, results, wins, and context for the period. Formaloo turns that submission into a branded PDF report, in your choice of three designs, and sends it straight to the client once your team approves it. Your whole team works from one portal that tracks every report by status, client, and assignee, so nothing gets lost between the analytics dashboard and the client's inbox.


When to use this

  • You're manually copying numbers out of an ads dashboard into a slide deck or doc for every client, every single month

  • Several account managers report on different clients and there's no single place to see what's been sent and what hasn't

  • Clients keep asking "where's my report" because sending one is a manual step someone has to remember

  • You want every client report to look consistently branded, no matter which account manager wrote it

  • You're adding retainer clients faster than your team can keep up with manual reporting, and it's starting to eat a full day every month


Step-by-step

Part 1 — Build the campaign report form with Magic Create

This is where the system starts. You'll build one form where account managers log a client's campaign performance for a reporting period: spend, results, wins, and context. Magic Create can generate the full field set for you in one pass, including the admin-only fields your team needs to manage review and status, straight from a text prompt.

1. Open Magic Create: From your Formaloo homepage, click Magic Create.

2. Describe your form: Paste a prompt describing the campaign report form. For example: "Build a campaign performance report form for a marketing agency. Ask which client and campaign it's for, the client's contact email, the reporting period start and end date, which platforms the campaign ran on, total spend, impressions, and clicks. Add a yes/no question asking whether the report includes paid conversion tracking, and only show conversions and conversion revenue when the answer is yes. Add long text fields for key wins, challenges or notes, and next period focus, plus a file upload for the client's logo. Also add three admin-only fields: a Report status dropdown with Draft, In review, and Sent to client, an Assignee field, and an Internal notes field."

3. Click Create: Formaloo generates the full form for you.

4. Verify your fields: Your form should look like this, Magic Create will have generated most of it. Verify each field and field ID matches exactly.

Field (question form)

Field ID

Type

Which client is this report for?

client_name

Short text

What's the client's contact email for sending this report?

client_contact_email

Short text

What's the campaign name?

campaign_name

Short text

What's the start date of this reporting period?

report_period_start

Date

What's the end date of this reporting period?

report_period_end

Date

Which platforms did this campaign run on?

platforms_used

Multi-select dropdown

What was the total spend for this period?

total_spend

Number

How many impressions did the campaign generate?

impressions

Number

How many clicks did the campaign generate?

clicks

Number

Does this report include paid conversion tracking?

has_conversion_tracking

Yes/No

How many conversions did the campaign generate?

conversions

Number

What was the total revenue or conversion value generated?

conversion_revenue

Number

What were the key wins or highlights this period?

key_wins

Long text

Any challenges or context the client should know about?

challenges_notes

Long text

What's the focus or plan for next period?

next_period_focus

Long text

Upload the client's logo for the branded report (optional)

client_logo_upload

File upload

5. Publish your form: Click Publish in the island bar at the top of the editor.


Part 2 — Set up the reporting logic with Magic Logic

Not every campaign has a paid conversion goal, so the form should only ask for conversion numbers when they apply. You'll use Magic Logic to describe this in plain English and let Formaloo build the rule.

1. Open Advanced logic: In the form settings panel on the right side of the form editor, click Advanced logic and go to the Logic tab.

2. Click Magic Logic: In the top right corner of the Logic panel, click Magic Logic.

3. Describe your rule: Paste this prompt: "When 'Does this report include paid conversion tracking?' is answered Yes, show 'How many conversions did the campaign generate?' and 'What was the total revenue or conversion value generated?' Hide both fields by default. Require both fields to be answered whenever the conversion tracking question is Yes."

4. Click Generate rules: Formaloo's AI previews the rule for you.

5. Approve the rule: Approve, redo, or cancel it until it matches what you described.

6. Save your logic: Click Save in the top right corner.


Part 3 — Create your email templates

You'll need three email templates: one that pings your team the moment a report is saved, one that flags it for admin review, and one that goes to the client once the report is approved. Each is written as an AI prompt, so Formaloo generates fresh, natural copy for every submission instead of a static block of text.

1. Open custom email templates: Click your profile icon, go to Apps and integrations, and find Custom email templates.

2. Create your first template: Click Add new template, name it "Report saved confirmation," set the subject to "Your campaign report for @client_name is saved," clear the body, add an AI block, and paste this prompt: "Write a warm, brief confirmation that this campaign report for @client_name (@campaign_name) covering @report_period_start to @report_period_end has been saved as a draft and is pending review. Keep it to 2-3 sentences, friendly and efficient." Save the template.

3. Create your second template: Repeat step 2 for a template named "New report ready for review," subject "New campaign report ready for review: @client_name," with this prompt: "Write a factual, minimal notification that a new campaign report for @client_name (@campaign_name) has been submitted and needs review before it's sent out. Reference the reporting period. Keep it under 4 sentences, no fluff."

4. Create your third template: Repeat step 2 for a template named "Campaign report ready for client," subject "Your @campaign_name performance report is ready," with this prompt: "Write a polished, client-facing email introducing the attached campaign performance report for @report_period_start to @report_period_end. Reference @key_wins briefly to set a positive tone, mention the full breakdown is in the attached PDF, and close with an offer to discuss further. Keep it to 4-5 sentences, professional and warm."

Here's the full content of all three templates for reference:

Report saved confirmation Subject: Your campaign report for @client_name is saved Body prompt: Write a warm, brief confirmation that this campaign report for @client_name (@campaign_name) covering @report_period_start to @report_period_end has been saved as a draft and is pending review. Keep it to 2-3 sentences, friendly and efficient. Trigger: sent to your agency's review inbox as soon as a report is submitted, configured in Part 4.

New report ready for review Subject: New campaign report ready for review: @client_name Body prompt: Write a factual, minimal notification that a new campaign report for @client_name (@campaign_name) has been submitted and needs review before it's sent out. Reference the reporting period. Keep it under 4 sentences, no fluff. Trigger: routed to your agency admin team through the forward/route option on the same notification, configured in Part 4.

Campaign report ready for client Subject: Your @campaign_name performance report is ready Body prompt: Write a polished, client-facing email introducing the attached campaign performance report for @report_period_start to @report_period_end. Reference @key_wins briefly to set a positive tone, mention the full breakdown is in the attached PDF, and close with an offer to discuss further. Keep it to 4-5 sentences, professional and warm. Trigger: sent automatically when Report status is changed to Sent to client, with the PDF attached, configured in Part 4.


Part 4 — Turn on notifications and the client-send trigger

Now connect those templates to the moments that should trigger them: a report being submitted, and an admin marking a report as sent.

1. Open Notify: From the right sidebar of the form builder, click Notify.

2. Turn on your team's save notification: Turn on Receive email notification, enter your agency's internal review inbox, and select Report saved confirmation as the template so your team gets a lightweight heads-up the moment a report comes in.

3. Route the review notification: Look for the option to forward or route the email, select New report ready for review as the template, and hit Publish so your agency admin team gets the fuller review notice.

4. Set up the client-send trigger: In Advanced logic, go to On Update and click Add rule. Set the condition to "When Report status is changed to Sent to client" and the action to send an email using the Campaign report ready for client template, to the address in "What's the client's contact email for sending this report?"

5. Attach the PDF: In the same rule, attach the generated campaign report PDF to the email, then hit Save.

Part 5 — Build your branded PDF report templates

This is the centerpiece of the whole workflow: the AI-generated, branded PDF that actually goes to the client. Build three design variants so agencies can pick whichever best matches their client's brand, since all three pull the same fields.

1. Open custom PDF templates: Click your profile icon, go to Apps and integrations, and click Custom PDF template.

2. Add your first design: Click Add new template, switch the editor to HTML view, and paste in the Classic design, a minimal white layout with muted labels and thin dividers. Name it "Campaign report: Classic" and save it.

3. Add your second design: Repeat step 2 for the Bold design, an accent-color header band with tinted stat cards, that agencies with a strong visual identity can use. Name it "Campaign report: Bold."

4. Add your third design: Repeat step 2 for the Dark design, a navy background with a gold accent, suited to premium or editorial-feeling retainer clients. Name it "Campaign report: Dark."

5. Set the default: Pick whichever design matches your agency's brand as the one attached in the Part 4 client-send rule. You can always swap it later since all three read from the same fields.

Here's the full HTML for all three designs. Paste whichever ones you want directly into the HTML view.

Campaign report: Classic

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Campaign Performance Report — Classic</title> <style>   @page { margin: 0; }   * { box-sizing: border-box; }   body {     margin: 0;     background: #ffffff;     font-family: 'Inter', system-ui, -apple-system, sans-serif;     color: #1a1a1a;   }   .page {     max-width: 760px;     margin: 0 auto;     padding: 60px 72px;   }   .header {     display: flex;     justify-content: space-between;     align-items: flex-start;     padding-bottom: 24px;     border-bottom: 1px solid #eaeaea;     margin-bottom: 32px;   }   .header .logo img {     max-height: 40px;     max-width: 160px;     object-fit: contain;   }   .header .doc-info { text-align: right; }   .doc-title {     font-size: 22px;     font-weight: 700;     margin: 0 0 4px 0;     letter-spacing: -0.01em;   }   .doc-meta {     font-size: 12px;     color: #999;     margin: 0;   }   .section { margin-bottom: 28px; }   .section-title {     font-size: 10px;     text-transform: uppercase;     letter-spacing: 0.08em;     color: #aaaaaa;     margin-bottom: 12px;     font-weight: 600;   }   .grid {     display: grid;     grid-template-columns: 1fr 1fr;     gap: 0 32px;   }   .field {     padding: 10px 0;     border-bottom: 1px solid #f0f0f0;   }   .field-label {     font-size: 11px;     color: #999;     margin-bottom: 2px;   }   .field-value {     font-size: 14px;     color: #1a1a1a;     font-weight: 500;   }   .field-full { grid-column: 1 / -1; }   .stat-value { font-size: 20px; font-weight: 700; }   .long-text-block {     font-size: 13px;     line-height: 1.6;     color: #333;     white-space: pre-wrap;   }   .inset {     background: #f9f9f9;     border-left: 3px solid #ddd;     padding: 16px 20px;     border-radius: 4px;     font-size: 13px;     line-height: 1.6;     color: #444;   }   .footer {     margin-top: 48px;     padding-top: 16px;     border-top: 1px solid #eaeaea;     display: flex;     justify-content: space-between;     font-size: 10px;     color: #bbb;   } </style> </head> <body>   <div class="page">      <div class="header">       <div class="logo">         <img src="@client_logo_upload" alt="Client logo" onerror="this.style.display='none'">       </div>       <div class="doc-info">         <p class="doc-title">Campaign performance report</p>         <p class="doc-meta">Ref: @campaign_name · @report_period_start – @report_period_end</p>       </div>     </div>      <div class="section">       <div class="section-title">Campaign overview</div>       <div class="grid">         <div class="field">           <div class="field-label">Client</div>           <div class="field-value">@client_name</div>         </div>         <div class="field">           <div class="field-label">Campaign</div>           <div class="field-value">@campaign_name</div>         </div>         <div class="field">           <div class="field-label">Reporting period</div>           <div class="field-value">@report_period_start – @report_period_end</div>         </div>         <div class="field">           <div class="field-label">Platforms</div>           <div class="field-value">@platforms_used</div>         </div>       </div>     </div>      <div class="section">       <div class="section-title">Performance metrics</div>       <div class="grid">         <div class="field">           <div class="field-label">Total spend</div>           <div class="field-value stat-value">@total_spend</div>         </div>         <div class="field">           <div class="field-label">Impressions</div>           <div class="field-value stat-value">@impressions</div>         </div>         <div class="field">           <div class="field-label">Clicks</div>           <div class="field-value stat-value">@clicks</div>         </div>         <div class="field">           <div class="field-label">Conversions</div>           <div class="field-value stat-value">@conversions</div>         </div>         <div class="field field-full">           <div class="field-label">Conversion revenue</div>           <div class="field-value stat-value">@conversion_revenue</div>         </div>       </div>     </div>      <div class="section">       <div class="section-title">Key wins & highlights</div>       <div class="long-text-block">@key_wins</div>     </div>      <div class="section">       <div class="section-title">Notes & context</div>       <div class="inset">@challenges_notes</div>     </div>      <div class="section">       <div class="section-title">Focus for next period</div>       <div class="long-text-block">@next_period_focus</div>     </div>      <div class="footer">       <span>Confidential — prepared for @client_name</span>       <span>[Agency name]</span>     </div>    </div> </body> </html>

Campaign report: Bold

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Campaign Performance Report — Bold</title> <style>   @page { margin: 0; }   * { box-sizing: border-box; }   :root { --accent: #4F46E5; --accent-soft: #EEF0FF; }   body {     margin: 0;     background: #ffffff;     font-family: 'Inter', system-ui, -apple-system, sans-serif;     color: #14141c;   }   .page {     max-width: 760px;     margin: 0 auto;     padding: 0 0 60px 0;   }   .header-band {     background: var(--accent);     color: #fff;     padding: 48px 72px 32px 72px;     display: flex;     justify-content: space-between;     align-items: flex-start;   }   .header-band .logo img {     max-height: 36px;     max-width: 150px;     object-fit: contain;     filter: brightness(0) invert(1);   }   .doc-title {     font-size: 24px;     font-weight: 800;     margin: 0 0 6px 0;     letter-spacing: -0.01em;   }   .doc-meta {     font-size: 12px;     color: #d9d7ff;     margin: 0;   }   .body-content { padding: 40px 72px 0 72px; }   .section { margin-bottom: 32px; }   .section-title {     font-size: 10px;     text-transform: uppercase;     letter-spacing: 0.08em;     color: var(--accent);     margin-bottom: 14px;     font-weight: 700;   }   .grid {     display: grid;     grid-template-columns: 1fr 1fr;     gap: 12px;   }   .card {     background: var(--accent-soft);     border-radius: 10px;     padding: 14px 16px;   }   .card-label {     font-size: 11px;     color: #6b6b8a;     margin-bottom: 4px;     font-weight: 600;   }   .card-value {     font-size: 15px;     color: #14141c;     font-weight: 600;   }   .stat-card { text-align: left; }   .stat-value { font-size: 24px; font-weight: 800; color: var(--accent); }   .field-full { grid-column: 1 / -1; }   .long-text-block {     font-size: 13px;     line-height: 1.7;     color: #333;     white-space: pre-wrap;   }   .inset {     background: #14141c;     color: #fff;     padding: 18px 22px;     border-radius: 10px;     font-size: 13px;     line-height: 1.7;   }   .footer {     margin: 48px 72px 0 72px;     padding-top: 16px;     border-top: 1px solid #eee;     display: flex;     justify-content: space-between;     font-size: 10px;     color: #bbb;   } </style> </head> <body>   <div class="page">      <div class="header-band">       <div class="logo">         <img src="@client_logo_upload" alt="Client logo" onerror="this.style.display='none'">       </div>       <div class="doc-info">         <p class="doc-title">Campaign performance report</p>         <p class="doc-meta">@campaign_name · @report_period_start – @report_period_end</p>       </div>     </div>      <div class="body-content">        <div class="section">         <div class="section-title">Campaign overview</div>         <div class="grid">           <div class="card">             <div class="card-label">Client</div>             <div class="card-value">@client_name</div>           </div>           <div class="card">             <div class="card-label">Campaign</div>             <div class="card-value">@campaign_name</div>           </div>           <div class="card">             <div class="card-label">Reporting period</div>             <div class="card-value">@report_period_start – @report_period_end</div>           </div>           <div class="card">             <div class="card-label">Platforms</div>             <div class="card-value">@platforms_used</div>           </div>         </div>       </div>        <div class="section">         <div class="section-title">Performance metrics</div>         <div class="grid">           <div class="card stat-card">             <div class="card-label">Total spend</div>             <div class="stat-value">@total_spend</div>           </div>           <div class="card stat-card">             <div class="card-label">Impressions</div>             <div class="stat-value">@impressions</div>           </div>           <div class="card stat-card">             <div class="card-label">Clicks</div>             <div class="stat-value">@clicks</div>           </div>           <div class="card stat-card">             <div class="card-label">Conversions</div>             <div class="stat-value">@conversions</div>           </div>           <div class="card stat-card field-full">             <div class="card-label">Conversion revenue</div>             <div class="stat-value">@conversion_revenue</div>           </div>         </div>       </div>        <div class="section">         <div class="section-title">Key wins & highlights</div>         <div class="long-text-block">@key_wins</div>       </div>        <div class="section">         <div class="section-title">Notes & context</div>         <div class="inset">@challenges_notes</div>       </div>        <div class="section">         <div class="section-title">Focus for next period</div>         <div class="long-text-block">@next_period_focus</div>       </div>      </div>      <div class="footer">       <span>Confidential — prepared for @client_name</span>       <span>[Agency name]</span>     </div>    </div> </body> </html>

Campaign report: Dark

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Campaign Performance Report — Dark</title> <style>   @page { margin: 0; }   * { box-sizing: border-box; }   :root { --gold: #D4AF6A; --bg: #111318; --card: #1a1d26; --muted: #8a8d98; }   body {     margin: 0;     background: var(--bg);     font-family: 'Inter', system-ui, -apple-system, sans-serif;     color: #f5f5f5;   }   .page {     max-width: 760px;     margin: 0 auto;     padding: 60px 72px;   }   .header {     display: flex;     justify-content: space-between;     align-items: flex-start;     padding-bottom: 24px;     border-bottom: 1px solid #2a2d38;     margin-bottom: 32px;   }   .header .logo img {     max-height: 40px;     max-width: 160px;     object-fit: contain;     filter: brightness(0) invert(1);   }   .header .doc-info { text-align: right; }   .doc-title {     font-size: 22px;     font-weight: 700;     margin: 0 0 4px 0;     letter-spacing: -0.01em;     color: #fff;   }   .doc-meta {     font-size: 12px;     color: var(--gold);     margin: 0;   }   .section { margin-bottom: 28px; }   .section-title {     font-size: 10px;     text-transform: uppercase;     letter-spacing: 0.1em;     color: var(--gold);     margin-bottom: 12px;     font-weight: 600;   }   .grid {     display: grid;     grid-template-columns: 1fr 1fr;     gap: 12px;   }   .field {     background: var(--card);     border: 1px solid #2a2d38;     border-radius: 8px;     padding: 14px 16px;   }   .field-label {     font-size: 11px;     color: var(--muted);     margin-bottom: 4px;   }   .field-value {     font-size: 14px;     color: #f5f5f5;     font-weight: 500;   }   .field-full { grid-column: 1 / -1; }   .stat-value { font-size: 20px; font-weight: 700; color: var(--gold); }   .long-text-block {     font-size: 13px;     line-height: 1.7;     color: #d8d8dc;     white-space: pre-wrap;   }   .inset {     background: var(--card);     border-left: 3px solid var(--gold);     padding: 16px 20px;     border-radius: 4px;     font-size: 13px;     line-height: 1.7;     color: #d8d8dc;   }   .footer {     margin-top: 48px;     padding-top: 16px;     border-top: 1px solid #2a2d38;     display: flex;     justify-content: space-between;     font-size: 10px;     color: #5c5f6a;   } </style> </head> <body>   <div class="page">      <div class="header">       <div class="logo">         <img src="@client_logo_upload" alt="Client logo" onerror="this.style.display='none'">       </div>       <div class="doc-info">         <p class="doc-title">Campaign performance report</p>         <p class="doc-meta">Ref: @campaign_name · @report_period_start – @report_period_end</p>       </div>     </div>      <div class="section">       <div class="section-title">Campaign overview</div>       <div class="grid">         <div class="field">           <div class="field-label">Client</div>           <div class="field-value">@client_name</div>         </div>         <div class="field">           <div class="field-label">Campaign</div>           <div class="field-value">@campaign_name</div>         </div>         <div class="field">           <div class="field-label">Reporting period</div>           <div class="field-value">@report_period_start – @report_period_end</div>         </div>         <div class="field">           <div class="field-label">Platforms</div>           <div class="field-value">@platforms_used</div>         </div>       </div>     </div>      <div class="section">       <div class="section-title">Performance metrics</div>       <div class="grid">         <div class="field">           <div class="field-label">Total spend</div>           <div class="field-value stat-value">@total_spend</div>         </div>         <div class="field">           <div class="field-label">Impressions</div>           <div class="field-value stat-value">@impressions</div>         </div>         <div class="field">           <div class="field-label">Clicks</div>           <div class="field-value stat-value">@clicks</div>         </div>         <div class="field">           <div class="field-label">Conversions</div>           <div class="field-value stat-value">@conversions</div>         </div>         <div class="field field-full">           <div class="field-label">Conversion revenue</div>           <div class="field-value stat-value">@conversion_revenue</div>         </div>       </div>     </div>      <div class="section">       <div class="section-title">Key wins & highlights</div>       <div class="long-text-block">@key_wins</div>     </div>      <div class="section">       <div class="section-title">Notes & context</div>       <div class="inset">@challenges_notes</div>     </div>      <div class="section">       <div class="section-title">Focus for next period</div>       <div class="long-text-block">@next_period_focus</div>     </div>      <div class="footer">       <span>Confidential — prepared for @client_name</span>       <span>[Agency name]</span>     </div>    </div> </body> </html>

Part 6 — Turn this into a portal for your team

Finally, wrap the form in a portal so account managers and agency admins each get their own view of the reporting pipeline, scoped to what's relevant to them.

1. Activate the portal: Go to your project's Settings (gear icon), select Activate portal, click Manage user, and open the Setup wizard.

2. Set up your user directory: Search for an existing user directory or add a new one, then click Next step. Every portal needs a directory to manage its registered users.

3. Connect the form: Add the profile field to connect your user directory to the campaign report form.

4. Add your user roles: In the User directory, click Edit profile fields, scroll to User roles, and add "Account manager" and "Agency admin" as roles.

5. Restrict "My reports" to each account manager's own submissions: On the "My reports" page's data block, switch to Edit mode, click Options, scroll to Manage access, select the submitter profile field, and toggle on Allow users to view only their data.

6. Restrict the assigned pipeline view: On "My assigned reports," switch to Edit mode, click Options, scroll to Manage access, select the Assignee field, and toggle on Allow assignees to view only their data.

7. Build out the remaining admin pages: Add "All reports" as a Kanban board grouped by Report status, plus "Reports by date," "Draft reports," "Ready for review," and "Sent to client" as Table data blocks connected to your campaign report form, filtered as described in your project pages plan.

8. Publish your portal.

Note: You can set a custom domain on your portal so it's fully branded to your agency, available on the Business plan or through the Brand removal add-on.


What you now have

A single campaign report form replaces the monthly ritual of copying spend and results out of an ads dashboard into a slide deck. Every account manager logs their client's numbers once, and Formaloo's AI turns that into a polished, branded PDF automatically, in whichever of the three designs fits the client. Your agency admin team reviews reports from one Kanban pipeline instead of chasing status over Slack, and the client-send email fires the moment a report is marked ready, so nothing sits in a drafts folder past its due date. As you add retainer clients, the same form and portal scale with you instead of adding another hour of manual work per client.


What's next


Ready-to-use templates

Don't build this from scratch, we've already put it together for you.

Service cost estimator for agencies — Lets clients estimate project costs before booking a call, with every estimate tracked from one folder view. Pair it with this reporting workflow to cover an agency client relationship from first quote to monthly results.

AI lead qualification & scoring form — Scores inbound demo requests with AI and routes hot and cold leads with conditional emails. Useful for the same agency team once campaign reports start surfacing upsell-ready clients.

Employee request workflow — Routes internal requests by type with approvals and team notifications. A good reference if you want to extend the same Assignee and Report status pattern to other internal agency processes.


Learn more how-tos

Did this answer your question?