Recruiter Codes Chrome Extension— A LinkedIn Fairy(-tale)!

Rafael Romo Mulas
Published in
12 min readDec 8, 2022


Two players have joined the game! — Photo by Mohammad Rahmani, fairy sprite by Elthen.

Check out my first blog series: Tech Recruiter Tries Coding, pt 1, pt 2, and pt 3!

Hello smart people, and welcome to a new series on coding by a non-coder!
This was originally going to be pt. 4 of the series linked above, but since the topic is quite new, and could need 2–3 articles itself… Better to start over!

And nothing better than a fresh start, to have an excuse for a fresh intro!

I’m a 36-year-old husband & father, and to introduce myself I will confess, not because it’s trendy (though it sure is 😆), that I’m “not neuro-typical”: I’ve got some kind of personality disorder they haven’t named yet, which I’d call “Jack-Of-All-Trades Disorder”: an urge to learn many different skills, but become “master of none”, as the saying goes.

This lead to life-spanning shenanigans, such as studying classical humanities in high school (ancient Latin & Greek included) only to 180 to hard science (rock hard, really) with a B.Sc. in Geology, all while telling myself that the C++ I had learned years before couldn’t give a nice job… 🙈

I then ditched and picked up a handful of extremely unrelated careers, but somehow gradually gravitated towards IT again, job after job.

Despite or because of this, I became “geekruiter” at FusionWorks, a software house that takes its people-centric and learning-driven values seriously, letting me relearn coding nearly from scratch, to automate tasks, and maybe come up with a nice product in the process, not sure... 😋

So here I am, telling the story of this latest crazy adventure, with objectives quite similar to those of my first article series: increasing productivity in recruitment, by coding a custom tool. After a deep dive into Google Apps Script, the next level of automation I could think was Chrome Extensions!

A Chrome Extension could transform the bland browser (and websites) I use every day into a productivity powerhouse for recruitment, and not only. Comment if you guess for what else, while reading; prize for the winner! (?)

But enough with the intro: I’ll tap into my N-th hobby/passion (fantasy RPGs writing), and tell you a story of peril, magic, and high adventure…

“A great evil, a peasant, and a magical helper”—A fairy tale is served!

Initially, my idea was to do full-on web scraping.
While it can be effective, it’s also against the TOS of most useful websites, so it can lead to legal, other than technical issues. Plus, together with good data, you’ll scrape a ton of junk as well. And having to separate good and junk data could end up hurting productivity, instead of increasing it!

So I figured what I needed was a “magical fairy”, that could extract data quickly from a LinkedIn profile, and send it to the Google Sheet I work with, so I wouldn’t have to copy-paste stuff multiple times, to track people.

Seems easy on paper, but if you want a Chrome Extension to directly send data to Google Sheets, you have to deal with OAuth for authentication, and the Sheets API to transfer data. It would have taken a long time to learn and code for a noob like me, defeating the original purpose of saving time!

What should NOT happen, when trying to increase productivity! 😂

Thinking of shortcuts, I realized the quickest was to make two things: a Chrome Extension, and a Google Web App to act as intermediary between it and Google Sheets. The reason is practical: the web app can be done in plain Google Apps Script (GAS), and since it can be set to impersonate myself, it doesn’t need authorization on sheets where I’m authorized. A win/win! ✌️

In my imaginary world, this would have been a Google Chrome fairy, doing some magic on LinkedIn, and sending the result to some other fantastic beast/character, living in Google’s vast oceans… 🧚‍♀️✨🧜‍♂️

So I said to my nerdy self, let’s roll these fantasy characters up! 🧙‍♂️

“The peasant picks up a sword” — Learning Chrome development!

First thing I needed was to present this crazy idea to my bosses (minus the fantasy characters: let’s not push it… 😆) and If you know FusionWorks or its bosses a bit, you know they were actually enthusiastic in their approval!

They enrolled me in this nice uDemy course, which had me developing a simple Chrome Extension in a matter of MINUTES, I swear!

The extension of the course was changing a website’s colors to create a dark mode. It wasn’t useful for my purpose, but it taught me the basics of DOM manipulation, which is what my fairy needed to do, to extract data.

Modern JavaScript uses the same rules of CSS to find elements in a page, so from a learning point of view, two birds with one stone: JavaScript + CSS!

After getting the hang of extensions in general, I dedicated an hour or so to exploring LinkedIn profiles with the Chrome Developer Tools console open, to find ways of extracting the most useful things, namely skills…

How “spaghetti” is this code, from 1 to 10..? 😅

Above is how I ended up doing it, which is surely not perfect, but is super fast both for the skills listed within job experiences (if they are added in the “canonical” way), and those in the actual Skills section.

By trial and error, I figured out the CSS selectors to get the rest of the data I wanted. I was nearly done, when I looked at the warning the Chrome Extensions page was giving me, and found out I was doing stuff in an outdated way: Manifest V3 was out, and I was using V2… Not nice!

Fortunately, the uDemy course had a guide to update to V3, albeit at the end of the course, which is why I needed to redo things, to a certain extent.

In my opinion, V2 was much more flexible and powerful, but that’s exactly why they made V3: they wanted to keep the power of extensions in check. That, plus quasi-philosophical changes to the Chrome APIs, that evidently needed enforcing to be taken seriously… But enough complaining! 😄

I switched to TypeScript as recommended (I talk about how cool it was in this older article, although chronologically I did it here first), and adapted to the V3 “framework”, which swaps out the concept of “background script” for that of “service worker”, which is actually a thing worth talking about.

The service worker is a one-file-only part of the extension that doesn’t live on a browser tab or HTML page, but in some kind of virtual window, which you can see only from the developer mode of the Extensions page.

Hopefully you’ll look at this window less than I did, when developing Chrome Extensions… 😅

Without going into Service Worker technicalities I don’t fully understand yet, the concept is that the worker can do everything that is independent from the pages you work with. In my case, dealing with the UI/UX (of Chrome, not mine) for example displaying success or failure to the user.

The so-called Content Scripts instead, can be many, and are tied to DOMs, though isolated. From their world, they can’t conflict with pages, which is good, but neither can they access browser stuff like bookmarks, or tabs.

This is important because very early on, I had decided this extension should work together with the bookmarks functionality! ⭐️

“A shortcut in the hero’s journey “— Repurposing existing UI!

As I mentioned in my latest article, I’m a big proponent of repurposing UIs that already exists, instead of creating new ones.

As a recruiter, I was already used to bookmarking profiles, since it’s an easy way to categorize them, and was the fastest way of copying links to a sheet (I’d export the bookmarks, then copy links in bulk from the export file).

Now, what I wanted was not for bookmarks to replace what I do on my Google Sheet (impossible), but rather to speed up data entry, adding all the info and categorization I want, without forcing me to copy/paste a bunch.

An added advantage of this, apart from the fact that I could use an existing UI element that I was already used to click, is that I could also transpose the categorization in bookmark folders to categorization in a Google Sheet column, namely the job position for which I’d be recruiting! ⭐

So the decision was made: the star icon would trigger the extension, when and only when the page would be that of a LinkedIn profile (at first…)

It’s easier said than done though, since I had to learn about asynchronous JavaScript to do this, although I kind of “cheated at it”, with nested callback functions instead of the more orthodox promises.

Emojis for object keys: yay or nay? 😄 And who’s Sylph?? Waaait for it…

Basically, since I needed both tabs and bookmarks, which are both requested asynchronously, I get them in nested callback functions, and do all the work inside the very last callback. Not neat, but it works!

Apart from console-logging, status messages would not be sent via annoying pop-ups like most extensions do, but through the tooltip of the extension icon itself: another piece of UI that I didn’t need to code myself!

Who the heck is “Lancer” you ask? Read on to find out! 😋

But as you might have guessed from the code, for simple state-tracking I actually ditched text: I’d have my fairy icon standing still when ready, flying when doing some work, and maybe with wings down if something happened. This proved to be easy and FUN!

Meet 🧚Sylph, the animated fairy of LinkedIn profiles’ sifting!

The idea is not new: animations have been used for state tracking since forever, and from the early 2000s, circling dots or lines became a standard. Repetitive movement = work being done. I say flapping wings qualify! 😋

I Googled around, and it turned out that, especially when coding V3 Chrome extensions, with Service Workers using a separate thread from the one of the web page in question, it’s possible to do good animations even without knowing a thing about the Canvas API, or the imageData interface.

What you need, if you want to save time, is just a numbered image for each frame of the animation. Fortunately, GIFs of flying fairies abound online, and you just need a free program like Gimp to save each frame separately.

Pixel art: always in the hearts of us 30-somethings! 🥲

The pixel art also inspired the extension’s name: since it would have sifted through LinkedIn data, and it was a fairy, my fantasy vocabulary thought of “sylph”: a type of aerial fairy that sounds close to the verb “sift”… 🤓

It was time to code the animation!
Now, trigger warning: developers will risk a heart attack reading the following. So take a breath, and stop drinking coffee for a second, not to mess up your new mechanical-switches RGB keyboard… Are you ready?
I’ll say it… At first, I used global variables to keep track of the state!!

I knew this was bad (although I admit I still can’t fully understand why 😅) so I later tried to reach a compromise not as advanced as a React component, but good enough to allow me to keep track of multiple tabs.
I went with a const object, still in the global scope… How does it count? 😬

I think it’s cool, because with one object I easily keep track of multiple animations. Which I need because often I’d start collecting data from one tab, and then switch tab to the next profile/tab to do the same in an instant.

With this solution, when I want an animation I create a new property on the object, using the ID of the tab as a key, and assigning it the number 1, standing for the 1st frame of animation (and conveniently a “truthy” value.)

I love this for some reason! Details below.

Then the recursive “SylphCasts” function changes the icon at a specified interval, adding the number from the object to the filename, so that frame1, frame2, etc. change as quickly as wanted, as long as that object key exists. So to stop the animation, I just delete the key with that tab’s “name”.

The most difficult line for me here, once I got around recursion (another painful confession, I didn’t know a function could call itself, before 😅) is the one with the % operator, to count from 1 to 10 and then reset back to 1.
I thought 2 lines were the minimum to achieve that, but then I went on StackOverflow… 😂 I just adapted what I found to make it always truthy.

Now, a better way to do this could be having the animation function and its state fully contained within the same object, or in instances of a class.
Should I refactor?
It will need more code: what is the advantage I miss? 🤔

I’ve probably been lazy on this because there was still a lot of work to do, namely the web app that actually puts the collected data on Google Sheets!

Meet 🧜‍♂️Lancer, the aquatic web app friend of Sylph!

Yes, I’m crazy with names, and yes, while Google calls it a web app, it’s actually more of a web service/API, since it has no frontend.
Its basic function is just to be given data via the Get method, and put it on my Google Sheet using regular GAS.

I know it’s a very unsafe method, because if you discovered the URL of this guy, you could just inject random stuff into my sheet (although it has some kind of “password” you won’t see here.) But I thought: what’s the worst that can happen? Someone adds candidates for me, as a prank? Be my guest! 😜

Jokes aside, for internal use I think it’s good enough, and it saves me from learning the Sheets API, and all the authentication processes my extension should go through, if I wanted to “talk” to Google Sheets directly from it.

Here’s the most important part I can share, that you can use for a DIY app:

You can even spot a “future” feature of the thing, making it work for lead generation too! 😎

It’s very important to make it return JSON output (even when it’s just text), or requests to it will be blocked by a nasty-but-necessary thing called CORS.

For the rest, it’s plain old GAS putting stuff in the sheet, so not much to show, but here’s a screenshot just for completeness’ sake:

Here there’s also a hint of doubles management, but I’ll talk about a much better version in the next article. 🤓

And this is all I have to say about that!

Some tricks are needed to make this work, of course, and if you can’t figure everything out, you can always ask FusionWorks to figure it out for you. 🫶

“The great evil was defeated! The end..?” — Lessons learned + teaser!

So at this point we have a nice way to quickly extract data from a profile (Showing a nice animation while doing so! 🤩) and put it on Google Sheets, using an intermediary web app/service, coded and deployed separately.

The biggest lesson here was that it’s actually easy to code a Chrome Extension. I think more code went into some of my Sheets automations, than in the final version of this extension AND the web app, together…

It was also the first time I discovered how much can be done by using existing UI, instead of creating more, a concept I continued to explore later.

Another lesson: sometimes, to reach a good result quickly you better create two small tools working together, rather than a single one trying too hard.

So satisfying! 🤩

As concluded in my previous blog, I think this “limbo” of coding extensions for programs that already exist, is really the best for productivity:

  • Better than existing solutions, because those never work the way you want, or if they do they’re crazy expensive.
  • Better than developing new apps, because those take a long time to make, and disrupt your existing workflow.

You’ll never stop using the browser and LinkedIn anyway, so why coding or using a separate app, when you can add functions to the browser itself?

But now you might ask: isn’t the functionality of this actually very limited? It is! It sorely needs a way to react to profiles already on the sheet, and then ways to kinda-modularly support other websites, without making a mess.

So next time you’ll see how I tackled these, hopefully, while entertained. 🙂

In the meantime, I invite you to consider joining the amazing FusionWorks team, if you like thinking outside the box, and learning while making something useful.

Happy coding!! ✌️



Rafael Romo Mulas

Jack of all trades: went from flying, to IT sales & recruitment, to software dev, but studied geology, and wannabe fantasy Role-Playing Games writer & creator!