<?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: Stephen Ingham</title>
    <description>The latest articles on DEV Community by Stephen Ingham (@stephen_ingham).</description>
    <link>https://dev.to/stephen_ingham</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%2F4042719%2F88588a3a-a602-4785-a097-0e41e817955f.jpg</url>
      <title>DEV Community: Stephen Ingham</title>
      <link>https://dev.to/stephen_ingham</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stephen_ingham"/>
    <language>en</language>
    <item>
      <title>Building a Job Post Recorder Agent with Google ADK and Composio</title>
      <dc:creator>Stephen Ingham</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:09:39 +0000</pubDate>
      <link>https://dev.to/stephen_ingham/building-a-job-post-recorder-agent-with-google-adk-and-composio-1kac</link>
      <guid>https://dev.to/stephen_ingham/building-a-job-post-recorder-agent-with-google-adk-and-composio-1kac</guid>
      <description>&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%2F0989qt76t6j4qwhparcd.jpg" 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%2F0989qt76t6j4qwhparcd.jpg" alt="Close-up of a person's hands typing on a laptop keyboard displaying a blank Google Docs document, with a blurred external monitor visible in the background at a desk." width="799" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@glenncarstenspeters?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Glenn Carstens-Peters&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/person-using-macbook-pro-npxXWgQ33ZQ?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;



&lt;p&gt;If you're on the job search like myself (at time of writing), I'm sure you'll know how critical it is to spend your time efficiently.&lt;/p&gt;

&lt;p&gt;You certainly don't want to spend additional time on admin, manually copy-pasting the details from the job description into whatever tool you use to track job applications.&lt;/p&gt;

&lt;p&gt;That's why I've made a tool to automate this part of the process, a &lt;strong&gt;multi-agent system&lt;/strong&gt; which auto extracts and pastes the relevant parts of job posts you care about directly into your Excel workbook sheet of choice!&lt;/p&gt;

&lt;p&gt;In this article, I cover my learnings figuring out how to work with &lt;strong&gt;Google ADK&lt;/strong&gt; and &lt;strong&gt;Composio&lt;/strong&gt; while building out this system - covering the architecture, the issues encountered and final takeaways for anyone wanting to tackle their own projects using these technologies!&lt;/p&gt;

&lt;p&gt;Hope you enjoy!&lt;/p&gt;




&lt;h2&gt;
  
  
  Google ADK/Composio Briefer
&lt;/h2&gt;

&lt;p&gt;Before we jump into things, I thought it would be worth giving a brief overview of Google's Agent Development Kit and Composio&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(I'm assuming in this overview a prior basic understanding of LLMs, agents, tools/MCP servers - if you're not already familiar with these terms they might be worth a google search first)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Google ADK
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://adk.dev/" rel="noopener noreferrer"&gt;Google's Agent Development Kit (ADK)&lt;/a&gt; is a scaffolding framework for building production fit agent-based systems.&lt;/p&gt;

&lt;p&gt;It offers a lot of flexibility around composing systems, especially v2.0 which introduced graph-based workflows which made it quite straightforward to implement conditional logic and more complex deterministic systems (compared to offerings of pre v2).&lt;/p&gt;

&lt;p&gt;The Google ADK supports SDKs across a number of languages, but the best supported SDK (alongside Go) at current is &lt;a href="https://adk.dev/get-started/python/" rel="noopener noreferrer"&gt;Python&lt;/a&gt;, so although I'm usually more of a TypeScript user, this dictated my language choice in this project.&lt;/p&gt;

&lt;p&gt;A handful of ADK-specific primitives come up repeatedly later in this article, so I'm defining them here sooner than later!:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;output_schema&lt;/code&gt;&lt;/strong&gt; - a Pydantic model you attach to an &lt;code&gt;Agent&lt;/code&gt; to constrain its final response to a specific JSON shape (e.g. &lt;code&gt;{header: value}&lt;/code&gt; pairs matching a spreadsheet's columns), rather than free-form text.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Callbacks&lt;/strong&gt; (e.g. &lt;code&gt;after_model_callback&lt;/code&gt;) - functions you hook into an agent's lifecycle to inspect or rewrite what it's about to do or just produced, without changing the agent's own instruction/prompt.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;ctx.state&lt;/code&gt;&lt;/strong&gt; - a shared key-value store (a "blackboard") passed through every node in a &lt;code&gt;Workflow&lt;/code&gt;, letting one node write a value (e.g. the extracted job URL) and a later node read it back, including interpolating it directly into another agent's instruction string.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these feel abstract right now, they'll click once you see them in context later in the article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composio
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://composio.dev/" rel="noopener noreferrer"&gt;Composio&lt;/a&gt; is a managed platform which makes tooling integrations accessible to AI agents.&lt;/p&gt;

&lt;p&gt;It offers toolkits for working with a large number of platforms, from GitHub to Bitwarden, and as well as being model agnostic it handles the auth to these platforms for you (which is a major reason why I chose to use it!).&lt;/p&gt;

&lt;p&gt;Generally speaking, it operates by exposing the signatures for a set of 'meta tools' to your agent which based on the query the agent sends finds and returns the signatures for the tools Composio believes you need to perform the requested task (avoiding context bloat that would otherwise occur in loading in the &lt;a href="https://composio.dev/toolkits" rel="noopener noreferrer"&gt;200+ tool signatures Composio could present&lt;/a&gt; to the agent!).&lt;/p&gt;

&lt;p&gt;Now you've got that context in mind, lets discuss the project!&lt;/p&gt;




&lt;h2&gt;
  
  
  Why an Agent-Based Approach?
&lt;/h2&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%2F0cgvh9z3zwhtyi6u3r5r.jpg" 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%2F0cgvh9z3zwhtyi6u3r5r.jpg" alt="A torn hole in brown kraft paper reveals a black laptop keyboard underneath, with the 'A' and 'I' keys visible through the opening." width="799" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@tinkerman?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Immo Wegmann&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-piece-of-cardboard-with-a-keyboard-appearing-through-it-vi1HXPw6hyw?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p&gt;Before I got started, I knew what I ultimately wanted should be quite straightforward.&lt;/p&gt;

&lt;p&gt;I needed the system to reliably navigate to a job URL, extract the details I was interested in and put them into a new row in my excel workbook.&lt;br&gt;
In an ideal world you wouldn't need agentic capabilities for this process at all. &lt;br&gt;
You should be able to just make a series of API calls, making a GET request to a given job post URL and look through structured HTML in the response for the fields.&lt;/p&gt;

&lt;p&gt;This however is not the reality for the combined following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Job Posts/descriptions don't all follow the same structure:&lt;/strong&gt;, &lt;br&gt;
so you'd need an approach which maps to every platform's different approach&lt;br&gt;
&lt;em&gt;i.e. terms used for salary, what headings they put role responsibilities under etc.&lt;/em&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web pages for job posts can be client-side rendered:&lt;/strong&gt;, &lt;br&gt;
so you may not be able to get the details you're wanting without spinning up a browser environment to render the page in&lt;br&gt;
&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Where pages are client side rendered important content may not be immediately visible:&lt;/strong&gt; &lt;br&gt;
so it will need user interaction to reveal, &lt;em&gt;(e.g. clicking a dropdown to find out the Role responsibilities)&lt;/em&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did a search for tools online and did find a few that as it turned out for me used agents to solve the problem.&lt;/p&gt;

&lt;p&gt;The only issue being that for one reason or another there was no good fit for my use case, i.e. namely platforms used an agent to get the content needed from a job post but forced using their bespoke spreadsheet software in doing so.&lt;/p&gt;

&lt;p&gt;This did lead to me however deciding that bringing AI agents into the extraction process would be a good way to tackle the unpredictability of job posting structures, given I provided appropriate tools, i.e. for rendering/searching page content in the browser.&lt;/p&gt;

&lt;p&gt;I therefore embarked after on mapping out the architecture!&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&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%2F57yanlmh2q9i6p232wlm.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%2F57yanlmh2q9i6p232wlm.png" alt="Flowchart of the job tracker agent's two-phase workflow. Left panel 'Startup &amp;amp; config': Start leads to launching a headless Playwright browser, then a decision to add a job entry or close out. Closing out shuts the browser context and halts. Adding an entry checks whether drive/workbook/sheet are cached in config.json - if cached, loads the details; if not, runs a setup wizard to pick drive, workbook, sheet, and column headers, then caches them. Both paths converge and continue to the right panel. Right panel 'Job entry': asks the user for a job posting URL, extracts fields from the page matching the sheet's headers, verifies the extraction against the live page (looping back to retry up to 2 times on failure), then on success writes the record as a new spreadsheet row via the Composio Excel MCP server, confirms the update to the user, and ends at Done." width="800" height="1268"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A high level abstraction of the system architecture, see &lt;a href="https://github.com/cyprste2717218/job_advert_recorder_agent/blob/master/ARCHITECTURE.md" rel="noopener noreferrer"&gt;ARCHITECTURE.MD in the GitHub Repo&lt;/a&gt; for the full diagram&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alongside some of my learnings from the &lt;a href="https://www.skills.google/paths/3545" rel="noopener noreferrer"&gt;‘Develop Agents with ADK’ skill path&lt;/a&gt; I was taking with Google and some research into &lt;a href="https://adk.dev/" rel="noopener noreferrer"&gt;the ADK docs&lt;/a&gt; I drew up a less detailed version of the architecture you see above.&lt;br&gt;
Less detailed as admittedly when I drew up the mermaid diagram my understanding of what was possible and the level of flexibility and efficiencies which could be achieved in composing ADK systems was lower.&lt;/p&gt;

&lt;p&gt;I realised this initially, but I decided as this was very much tooling I wanted to meet just my own requirements this was a less relevant concern for me at first (although in retrospect it certainly should have been more relevant!).&lt;/p&gt;

&lt;p&gt;I opted to compose the system using a graph based approach in the Google ADK, due to the flexibility offered in declaratively implementing routing and first class support for defining non-agent nodes.&lt;br&gt;
The additional reason being that it would reduce the number of breaking changes I’d have to work through if I wanted to take in any further in future, very much seeming like the sensible option.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1). Handling Composio tradeoffs
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Using Composio was a decision that I made under the assumption that it would be safer and less effort than having to implement auth management myself to OneDrive and Excel.&lt;br&gt;
I believe this has held true, however it introduced a large element of non-determinism to the system that had to be handled (not to mention token cost, although it's on the lighter side with using non-frontier models here).&lt;/p&gt;

&lt;p&gt;The two major risks posed were as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wide scope for unrestricted tooling access&lt;/strong&gt; if the agent decided for some reason an irrelevant tool would solve the problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unpredictable response formats&lt;/strong&gt; in case of runtime errors and even in successful operations!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The first issue was the far easier issue to fix&lt;/strong&gt;, you could reduce the scope of actions via tools an agent could take through giving a specific allowlist in the Composio SDK to restrict the toolsets and list only the required tools.&lt;br&gt;
Layering on top of this you can provide a clear and specific &lt;code&gt;instruction&lt;/code&gt; explicitly mandating specific tools for specific agent nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second issue of response formats&lt;/strong&gt; was more difficult to tackle.&lt;br&gt;
Even with a clear and specific &lt;code&gt;instruction&lt;/code&gt; and &lt;code&gt;output_schema&lt;/code&gt; for the agent to adhere to you can't guarantee what the output format will be, whether the process is a success or an error occurs.&lt;/p&gt;

&lt;p&gt;Frankly, there are far too many ways an agent could output that an errors occurred in terms of prose even with a fixed set of failure modes, the same applies to success messages also!&lt;/p&gt;

&lt;p&gt;My solution to this was to leverage the ADK's &lt;code&gt;after_model_callback&lt;/code&gt; after the agent node had finished all tool calls, intercepting any errors and capture them in the session state under a key of &lt;code&gt;job_spec_details_error&lt;/code&gt; (and in which case I set the state &lt;code&gt;job_spec_details&lt;/code&gt; to &lt;code&gt;{}&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Only then is the output JSON validated against the Pydantic schema in &lt;code&gt;output_schema&lt;/code&gt;.&lt;br&gt;
A subsequent node would then check for errors under &lt;code&gt;job_spec_details_error&lt;/code&gt; and surfaces any as user messages before rerunning the agent and (ideally) not running into the issue again.&lt;br&gt;
I set a max retry count of 3 attempts before the system opted to raise an error and exit.&lt;/p&gt;

&lt;p&gt;While there are more flexible approaches and more configuration that can be done to improve the system, this approach was sufficient for my purposes.&lt;br&gt;
For example, it would be handy getting an agent to do some light troubleshooting to determine the issue involved and informing the user what's needed to resolve, e.g. API rate limit being met so needing to wait for a period of time before retrying.&lt;/p&gt;




&lt;h2&gt;
  
  
  2). Flaky gemini models
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
During development, I've ran a number of times into unavailability of Gemini's flash models.&lt;br&gt;
I can't say I'm too surprised in retrospect as I created a Gemini API Key on the free tier within a test GCP project in Google AI Studio.&lt;/p&gt;

&lt;p&gt;However, it's been pretty frustrating when I've been wanting to do some quick tests and been locked out due to model unavailability due to spikes in external usage.&lt;/p&gt;

&lt;p&gt;Unless you upgrade the billing tier, I'd recommend as a quicker workaround setting up an environment variable(s) and using these within your agent definitions.&lt;br&gt;
It doesn't solve the root problem but it can be a quicker approach than switching out all agents &lt;code&gt;model&lt;/code&gt; fields by hand!&lt;/p&gt;




&lt;h2&gt;
  
  
  3). Mitigating anti-bot measures
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
A fair reason why I believe I struggled to find a tool to fix this problem originally is because using agents, web scraping tools or otherwise requires doing your best to mitigate against anti-bot detections and frankly sometimes this isn't always possible.&lt;/p&gt;

&lt;p&gt;LinkedIn and Indeed are especially tricky to work around in my experience so far, which can be frustrating when those can be the places you want to target &lt;em&gt;(although frankly I wouldn't advise putting too much focus on Linkedin for searching and making actual job applications - but that's a whole other conversation!)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I created a few custom tools for an agent to use with Playwright which controls a headless chromium instance to navigate and read content from.&lt;br&gt;
This worked all well and fine on more lenient sites for extracting job details, but when it came to Linkedin and Indeed it was a much different story.&lt;/p&gt;

&lt;p&gt;With Linkedin, I've decided to give it a pass for now as unless I can use an authentic &lt;code&gt;user-agent&lt;/code&gt; (via browser extension control, e.g. chrome) it's pretty tricky. &lt;/p&gt;

&lt;p&gt;In addition, you need to be logged in to Linkedin to view jobs which can you put in a tricky situation if you get caught out - perhaps more so than being logged out and getting IP-banned.&lt;/p&gt;

&lt;p&gt;However, for other sites I've found using the &lt;a href="https://pypi.org/project/playwright-stealth/" rel="noopener noreferrer"&gt;&lt;code&gt;playwright-stealth&lt;/code&gt;&lt;/a&gt; python library to be a good mitigation in terms of hiding the tell tale signs of a headless browser fingerprint.&lt;/p&gt;

&lt;p&gt;I'll admit this is an area I'm still learning about so I've yet to cover all practical bases here but it's helped me in part here.&lt;br&gt;
Will do my best to update this article around this topic with new approaches as I get stuff sorted here!&lt;/p&gt;




&lt;h2&gt;
  
  
  Final takeaways/conclusion
&lt;/h2&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%2Faukc4um7s4yo5u9nr17l.jpg" 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%2Faukc4um7s4yo5u9nr17l.jpg" alt="A tall stack of paperback books with worn, cream-colored page edges, viewed close-up from the side, sitting on a wooden table against a plain light gray background. The book spines are various colors including blue and red." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@sharonmccutcheon?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Alexander Grey&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/assorted-books-on-wooden-table-eMP4sYPJ9x0?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
If you're tackling an agent-based project with Google ADK and Composio, I'd recommend the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔒 &lt;strong&gt;Allowlist needed tools/toolkits only&lt;/strong&gt; when using Composio&lt;/li&gt;
&lt;li&gt;🛡️ &lt;strong&gt;Use ADK agent callbacks&lt;/strong&gt; to safely handle output format errors&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Enable quick model switch-outs&lt;/strong&gt; with environment variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hope you enjoyed reading and the above guidance can be of help for you if you embark on your own project with Google ADK and Composio.&lt;/p&gt;

&lt;p&gt;If you do, feel free to comment below as would love to check them out.&lt;br&gt;
Additionally, if you have any further questions around this area &lt;a href="https://www.linkedin.com/in/stephen-ingham-/" rel="noopener noreferrer"&gt;send me a message on LinkedIn&lt;/a&gt; or comment below and I'll answer to the best of my knowledge!&lt;/p&gt;

&lt;p&gt;As an aside, if you'd like to checkout the project check out the repo link below.&lt;br&gt;
I've also attached docs links for some of the topics I've covered here for further reading.&lt;/p&gt;

&lt;p&gt;Hope you have a great day!&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/cyprste2717218/job_advert_recorder_agent" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adk.dev/" rel="noopener noreferrer"&gt;Google ADK Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.composio.dev/docs" rel="noopener noreferrer"&gt;Composio Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Learnings &amp; Pitfalls of Building a Chrome Extension</title>
      <dc:creator>Stephen Ingham</dc:creator>
      <pubDate>Tue, 28 Jul 2026 17:42:07 +0000</pubDate>
      <link>https://dev.to/stephen_ingham/the-learnings-pitfalls-of-building-a-chrome-extension-l1i</link>
      <guid>https://dev.to/stephen_ingham/the-learnings-pitfalls-of-building-a-chrome-extension-l1i</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you’ve worked in software development, then you may be familiar with the situation during a busy feature or maintenance sprint where pull requests are being produced left right and centre across a range of your team’s git repositories.&lt;/p&gt;

&lt;p&gt;It can be tricky to determine immediately which repositories have active pull requests in need of review when using only conventional tools, e.g. slack threads, email notifications, without a good deal of searching first.&lt;/p&gt;

&lt;p&gt;To tackle this issue, I designed and developed a utility chrome extension which allows you to quickly check for any active pull requests on repositories you choose to track. As a result, standing to save you time in the process.&lt;/p&gt;

&lt;p&gt;In this blog I will cover my core learnings while developing this extension. However I’ll first give a brief overview of what a chrome extension is and how it compares to your standard web application for context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chrome Extensions vs. Web Applications
&lt;/h2&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%2Fj3cyek35q1aatgfqn131.jpg" 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%2Fj3cyek35q1aatgfqn131.jpg" alt="Decorative image of chrome browser icon" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Photo by Rubaitul Azad on Unsplash&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is a Chrome Extension?
&lt;/h2&gt;

&lt;p&gt;A chrome extension is a small piece of software that runs in the browser, which adds new features or modifies existing functionality. They sometimes offer a user interface (popup) accessible from the toolbar and can be used to perform tasks such as blocking ads or providing translation assistance etc. and are available from the Chrome Web Store. &lt;/p&gt;




&lt;h3&gt;
  
  
  The Similarities:
&lt;/h3&gt;

&lt;p&gt;Fundamentally, a chrome extension uses the same web technologies as any other web application.&lt;/p&gt;

&lt;p&gt;You have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML, CSS and JavaScript files:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These describe your user interface and the logic for your application for execution within the browser environment, as they do for a web application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript Frameworks/Libraries and Build Tools:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While not essential, just as it is possible to write web apps without them you are able to use JavaScript frameworks/libraries such as React/Vue alongside build tools such as webpack, babel etc.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Differences:
&lt;/h3&gt;

&lt;p&gt;The main differences between web apps and chrome extensions lie in how each type of application utilizes the browser under the hood, and the need for an extension to have a manifest.json file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Difference 1: The manifest.json&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At a minimum, a chrome extension needs a manifest.json file. This tells the browser everything it needs to know to load, configure and display the extension. Without this file, the extension would cease to exist.&lt;/p&gt;

&lt;p&gt;For example, the manifest.json is needed to inform the browser that the extension needs permission to use storage APIs to store/retrieve data from within the browser’s local storage for instance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Difference 2: Background and Content Scripts&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to manipulate the browser, your extension needs a service worker.&lt;/p&gt;

&lt;p&gt;For example, a service worker can be responsible for detecting changes to an extension’s local storage or rearranging chrome tabs for example.&lt;/p&gt;

&lt;p&gt;A service worker runs as a thread within your browser to execute APIs enabling these activities. However, a service worker cannot directly alter the DOM of webpages you have open.&lt;/p&gt;

&lt;p&gt;This is where a content script comes in (or an offscreen document, which won’t be discussed in this blog).&lt;/p&gt;

&lt;p&gt;A content script provides APIs which can alter the content/styling of webpages. To coordinate changes to the DOM based on events detected within the service worker as described, a communication chain must be established. This is done by setting up a message passing API between the service worker and content script.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Power of Browser Extensions:
&lt;/h2&gt;

&lt;p&gt;Compared to a typical web application, setting up a communication chain to manipulate the DOM after a change is detected in the browser requires more work to achieve.&lt;/p&gt;

&lt;p&gt;However, the core benefit of this setup is that a chrome extension can access a far greater range of additional, more powerful APIs than a web application can.&lt;/p&gt;

&lt;p&gt;For example, a chrome extension can use the Extensions API for handling system access, tab manipulation, searching browser history, network request control functionality among many other purposes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reduced API access in Web Applications
&lt;/h2&gt;

&lt;p&gt;Web applications can gain access to a smaller set of browser APIs which pose a lesser risk, such as allowing access to a device’s geolocation.&lt;/p&gt;

&lt;p&gt;The reasoning for this architectural difference is due to security risks that web applications cannot mitigate against. Namely, malicious client-side code could be hosted on a web server and stand to use APIs in a myriad of harmful ways against the user.&lt;/p&gt;

&lt;p&gt;This is a far less likely occurrence for a chrome extension which would undergo a thorough review process before release on the Chrome Web Store.&lt;/p&gt;

&lt;p&gt;The manner of gaining access to the DOM and the more powerful functions chrome extension APIs can perform form the dividing line here. Now we have covered the fundamentals let’s discuss my learnings!&lt;/p&gt;




&lt;h2&gt;
  
  
  Tip #1 - Invest in creating a Mock for your API Integration
&lt;/h2&gt;

&lt;p&gt;One of my first development mistakes was in accidentally making too many unauthenticated API requests to GitHub in lieu of working with a mock/double.&lt;/p&gt;

&lt;p&gt;As I had experienced some initial trouble in setting up a testing environment for the extension, I had consequently put off setting in place a double for the GitHub REST API I could use during local development.&lt;/p&gt;

&lt;p&gt;As a result, I was occasionally getting IP locked from making too many unauthenticated requests to the API.&lt;/p&gt;

&lt;p&gt;This being, due to the frequency at which I was manually making and then checking small source code changes hadn’t inadvertently introduced bugs during core user flows, which naturally involved making lots of API requests.&lt;/p&gt;

&lt;p&gt;The delays encountered due to this were fairly brief as I thankfully recognized them as they came through and aimed to be more intentional with my API calls.&lt;/p&gt;

&lt;p&gt;However investing the time in setting up a mock, i.e. dockerized JSON server image or the like, could have definitely saved some time in retrospect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tip #2 - Use Plasmo to speed up your development workflow
&lt;/h2&gt;

&lt;p&gt;Something else I wished I invested the time in, was looking further into finding tooling for showing your updated chrome extension’s popup as soon as modifications were made. Namely, I was looking for what many web framework build tools offer, the ability to provision a development server for your chrome extension.&lt;/p&gt;

&lt;p&gt;When I started out this project, I did some initial research into this area and wasn’t able to find anything fulfilling this role. My approach was to manually reload the unpacked extension within the developer console after each new build.&lt;/p&gt;

&lt;p&gt;I evidently didn’t search hard enough initially, as it was only later I discovered tools such as &lt;a href="https://docs.plasmo.com/" rel="noopener noreferrer"&gt;Plasmo&lt;/a&gt; existed for this purpose.&lt;/p&gt;

&lt;p&gt;In retrospect, using Plasmo to channel a dev server from my build tool, Vite, would have saved me a lot of time, allowing for a quicker development flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tip #3 - Use the chrome.alarms API to ensure reliable timer/alarms functionality
&lt;/h2&gt;

&lt;p&gt;Later in development I realised I was likely approaching a stage at which I’d be ready to prepare the extension for release on the Chrome Web Store.&lt;/p&gt;

&lt;p&gt;However there was still one major issue with functionality that had persisted.&lt;/p&gt;

&lt;p&gt;Contrary to my initial assumptions, when the extension popup was closed the mechanism to automatically fetch active pull requests on a user’s tracked repositories also stopped running. This went against the feature requirement I had in providing the user with up to date tracking functionality.&lt;/p&gt;

&lt;p&gt;I had naively assumed at first that any code I wrote in the background script would continue to run persistently regardless of whether the chrome extension popup was loaded or not. For example, code running on a loop through the web setInterval API.&lt;/p&gt;

&lt;p&gt;This being as this was my experience and understanding in the context of background processes for loaded but inactive chrome tabs.&lt;/p&gt;

&lt;p&gt;I presumed the same would apply for a chrome extension which was at the very least loaded in the user’s browser.&lt;/p&gt;

&lt;p&gt;This however is not the case as I found the logic I had created to automate fetching of updated pull request numbers would at most run for 30 seconds before the thread died and with it any future scheduled fetches.&lt;/p&gt;

&lt;p&gt;This is due to a behavior requirement of extensions using Manifest V3 which enforces service worker threads to be short lived. The aim of this design decision is to minimize browser resource usage, only running continuously if triggering API-related events occurs often enough in the worker.&lt;/p&gt;

&lt;p&gt;Thankfully, there is an easy solution to ensure processes occur reliably to a set schedule. This is provided by the &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/alarms" rel="noopener noreferrer"&gt;chrome.alarms&lt;/a&gt; API which offers alarm functionality for creating and triggering resilient alarms.&lt;/p&gt;

&lt;p&gt;These alarms awaken the service worker through configured alarm event listeners, which in my case ensured the extension continuously made GitHub REST API calls at set intervals, e.g. every 1 or 5 minutes.&lt;/p&gt;

&lt;p&gt;While admittedly a setback, through this experience I learnt a valuable piece of domain knowledge in chrome extension development.&lt;/p&gt;

&lt;p&gt;Additionally, it also illustrated the importance of vetting your technical assumptions before planning and implementing architecture. It certainly would have saved me a good deal of time on reflection!&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Learnings/Takeaways
&lt;/h2&gt;

&lt;p&gt;In summary, when undertaking a chrome extension development project ensure you follow these steps to make your life easier!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🎭 Mock your API&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🛠️ Use Plasmo for faster iterations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⏰ Use chrome.alarms for reliable scheduling&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this helps you to avoid some of the development pitfalls I ran into when you tackle your own projects!&lt;/p&gt;

&lt;p&gt;If you have any questions or thoughts on what I’ve run through here, feel free to drop me a message in the article comments below or directly to my &lt;a href="https://www.linkedin.com/in/stephen-ingham-/" rel="noopener noreferrer"&gt;Linkedin profile&lt;/a&gt; as I’m happy to help!&lt;/p&gt;

&lt;p&gt;Alternatively, if you’d like to check out the repo for the extension the link is below alongside docs you may find interesting.&lt;/p&gt;

&lt;p&gt;Hope you have a great rest of your day!&lt;/p&gt;

&lt;p&gt;Stephen&lt;/p&gt;

&lt;p&gt;🔗 Links:&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/cyprste2717218/github-prs-chrome-extension" rel="noopener noreferrer"&gt;https://github.com/cyprste2717218/github-prs-chrome-extension&lt;/a&gt;&lt;br&gt;
Plasmo Docs: &lt;a href="https://docs.plasmo.com/" rel="noopener noreferrer"&gt;https://docs.plasmo.com/&lt;/a&gt;&lt;br&gt;
Chrome Extension Docs: &lt;a href="https://developer.chrome.com/docs/extensions" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/extensions&lt;/a&gt;&lt;/p&gt;

</description>
      <category>extensions</category>
      <category>github</category>
    </item>
  </channel>
</rss>
