🚀Creating a resume builder with NextJS, Trigger.dev and GPT4🔥✨
Tags: webdev, javascript, programming, tutorial
Published: 2023-11-08 12:07:21

## TL;DR

In this article, you will learn how to create a resume builder using NextJS, Trigger.dev, Resend, and OpenAI. 😲

- Add basic details such as First name, last name, and last places of work.
- Generate details such as Profile Summary, Work History, and Job Responsibilities.
- Create a PDF that contains all the information.
- Send everything to your email


![Monkey Table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23k6hee187s62k8y1dmd.gif)


***

## Your background job platform 🔌

[Trigger.dev](https://trigger.dev/) is an open-source library that enables you to create and monitor long-running jobs for your app with NextJS, Remix, Astro, and so many more!

&nbsp;

[![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev)

Please help us with a star 🥹.
It would help us to create more articles like this 💖

{% cta https://github.com/triggerdotdev/trigger.dev %} Star the Trigger.dev repository ⭐️ {% endcta %}


<hr />

## Let's set it up 🔥

Set up a new project with NextJS

```bash
npx create-next-app@latest
```

We are going to create a simple form with basic information such as:
- First name
- Last name
- Email address
- Your profile picture
- And the experience you have until today!


![Inputs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/01mmvn0lvw1p1i4knoa8.png)

We are going to work with NextJS's new app router.
Open `layout.tsx` and add the following code

```jsx
import { GeistSans } from "geist/font";
import "./globals.css";

const defaultUrl = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : "http://localhost:3000";

export const metadata = {
  metadataBase: new URL(defaultUrl),
  title: "Resume Builder with GPT4",
  description: "The fastest way to build a resume with GPT4",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className={GeistSans.className}>
      <body className="bg-background text-foreground">
        <main className="min-h-screen flex flex-col items-center">
          {children}
        </main>
      </body>
    </html>
  );
}
```

We are basically setting the layout for all the pages (even though we have only one page.)
We set the basic page metadata, background, and global CSS elements.

Next, let's open our `page.tsx` and add the following code:

```jsx
    <div className="flex-1 w-full flex flex-col items-center">
      <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
        <div className="w-full max-w-6xl flex justify-between items-center p-3 text-sm">
          <span className="font-bold select-none">resumeGPT.</span>
        </div>
      </nav>

      <div className="animate-in flex-1 flex flex-col opacity-0 max-w-6xl px-3">
        <Home />
      </div>
    </div>
```

This sets the headline of our resumeGPT and the main home components.

<hr />

## The easiest way to build forms

The easiest way to save the form information and validate our fields is to use react-hook-form.

We are going to upload a profile picture.
For that, we can't use JSON-based requests.
We will need to convert the JSON into a valid form-data.

So let's install them all!

```bash
npm install react-hook-form object-to-formdata axios --save
```

Create a new folder called components add a new file called `Home.tsx`, and add the following code:

```jsx
"use client";

import React, { useState } from "react";
import {FormProvider, useForm} from "react-hook-form";
import Companies from "@/components/Companies";
import axios from "axios";
import {serialize} from "object-to-formdata";

export type TUserDetails = {
  firstName: string;
  lastName: string;
  photo: string;
  email: string;
  companies: TCompany[];
};

export type TCompany = {
  companyName: string;
  position: string;
  workedYears: string;
  technologies: string;
};

const Home = () => {
  const [finished, setFinished] = useState<boolean>(false);
  const methods = useForm<TUserDetails>()

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = methods;

  const handleFormSubmit = async (values: TUserDetails) => {
    axios.post('/api/create', serialize(values));
    setFinished(true);
  };

  if (finished) {
    return (
        <div className="mt-10">Sent to the queue! Check your email</div>
    )
  }

  return (
    <div className="flex flex-col items-center justify-center p-7">
      <div className="w-full py-3 bg-slate-500 items-center justify-center flex flex-col rounded-t-lg text-white">
        <h1 className="font-bold text-white text-3xl">Resume Builder</h1>
        <p className="text-gray-300">
          Generate a resume with GPT in seconds 🚀
        </p>
      </div>
      <FormProvider {...methods}>
        <form
          onSubmit={handleSubmit(handleFormSubmit)}
          className="p-4 w-full flex flex-col"
        >
          <div className="flex flex-col lg:flex-row gap-4">
            <div className="flex flex-col w-full">
              <label htmlFor="firstName">First name</label>
              <input
                type="text"
                required
                id="firstName"
                placeholder="e.g. John"
                className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent"
                {...register('firstName')}
              />
            </div>
            <div className="flex flex-col w-full">
              <label htmlFor="lastName">Last name</label>
              <input
                type="text"
                required
                id="lastName"
                placeholder="e.g. Doe"
                className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent"
                {...register('lastName')}
              />
            </div>
          </div>
          <hr className="w-full h-1 mt-3" />
          <label htmlFor="email">Email Address</label>
          <input
            type="email"
            required
            id="email"
            placeholder="e.g. john.doe@gmail.com"
            className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent"
            {...register('email', {required: true, pattern: /^\S+@\S+$/i})}
          />
          <hr className="w-full h-1 mt-3" />
          <label htmlFor="photo">Upload your image 😎</label>
          <input
            type="file"
            id="photo"
            accept="image/x-png"
            className="p-3 rounded-md outline-none border border-gray-500 mb-3"
            {...register('photo', {required: true})}
          />
          <Companies />
          <button className="p-4 pointer outline-none bg-blue-500 border-none text-white text-base font-semibold rounded-lg">
            CREATE RESUME
          </button>
        </form>
      </FormProvider>
    </div>
  );
};

export default Home;
```

You can see that we start with `"use client"` which basically tells our component that it should run on the client only.

Why do we want client only?
React states (input changes) are available only on the client side.

We set two interfaces, `TUserDetails` and `TCompany`. They represent the structure of the data we are working with.

We use `useForm` with `react-hook-form`. It creates a local state management to our inputs and allows us to update and validate our fields easily. you can see that in every `input`, there is a simple `register` function that specific the input name and validation and registers it to the managed state.

This is cool as we don't need to play with things like `onChange`

You can also see that we use `FormProvider`, that's important as we want to have the Context of `react-hook-form` in children components.

We also have a method called `handleFormSubmit`. That's the method that is called once we submit the form. You can see that we use the `serialize` function to convert our javascript object to FormData and send a request to the server to initiate the job with `axios`.

And you can see another component called `Companies`. That component will let us specify all the companies we worked for.

So let's work on it.
Create a new file called `Companies.tsx`
And add the following code:

```jsx
import React, {useCallback, useEffect} from "react";

import { TCompany } from "./Home";
import {useFieldArray, useFormContext} from "react-hook-form";

const Companies = () => {
  const {control, register} = We();
  const {fields: companies, append} = useFieldArray({
    control,
    name: "companies",
  });

  const addCompany = useCallback(() => {
    append({
      companyName: '',
      position: '',
      workedYears: '',
      technologies: ''
    })
  }, [companies]);

  
useEffect(() => {
    addCompany();
  }, []);

  return (
    <div className="mb-4">
      {companies.length > 1 ? (
        <h3 className="font-bold text-white text-3xl my-3">
          Your list of Companies:
        </h3>
      ) : null}
      {companies.length > 1 &&
        companies.slice(1).map((company, index) => (
          <div
            key={index}
            className="mb-4 p-4 border bg-gray-800 rounded-lg shadow-md"
          >
            <div className="mb-2">
              <label htmlFor={`companyName-${index}`} className="text-white">
                Company Name
              </label>
              <input
                type="text"
                id={`companyName-${index}`}
                className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                {...register(`companies.${index}.companyName`, {required: true})}
              />
            </div>

            <div className="mb-2">
              <label htmlFor={`position-${index}`} className="text-white">
                Position
              </label>
              <input
                type="text"
                id={`position-${index}`}
                className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                {...register(`companies.${index}.position`, {required: true})}
              />
            </div>

            <div className="mb-2">
              <label htmlFor={`workedYears-${index}`} className="text-white">
                Worked Years
              </label>
              <input
                  type="number"
                  id={`workedYears-${index}`}
                  className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                  {...register(`companies.${index}.workedYears`, {required: true})}
              />
            </div>
            <div className="mb-2">
              <label htmlFor={`workedYears-${index}`} className="text-white">
                Technologies
              </label>
              <input
                  type="text"
                  id={`technologies-${index}`}
                  className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                  {...register(`companies.${index}.technologies`, {required: true})}
              />
            </div>
          </div>
        ))}
        <button type="button" onClick={addCompany} className="mb-4 p-2 pointer outline-none bg-blue-900 w-full border-none text-white text-base font-semibold rounded-lg">
          Add Company
        </button>
    </div>
  );
};

export default Companies;
```

We start with `useFormContext`, which allows us to get the context of the parent component.

Next, we use `useFieldArray` to create a new state called companies. It is an array for all the companies that we have.

In the `useEffect`, we add the first item of the array to iterate over it.

When clicking on `addCompany`, it will push another element to the array.

We have finished with the client 🥳

<hr />

## Parse the HTTP request

Remember that we send a `POST` request to `/api/create`?
Let's go to our app/api folder and create a new folder called `create` inside that folder, create a new file called `route.tsx` and paste the following code:

```javascript
import {NextRequest, NextResponse} from "next/server";
import {client} from "@/trigger";

export async function POST(req: NextRequest) {
    const data = await req.formData();
    const allArr = {
        name: data.getAll('companies[][companyName]'),
        position: data.getAll('companies[][position]'),
        workedYears: data.getAll('companies[][workedYears]'),
        technologies: data.getAll('companies[][technologies]'),
    };

    const payload = {
        firstName: data.get('firstName'),
        lastName: data.get('lastName'),
        photo: Buffer.from((await (data.get('photo[0]') as File).arrayBuffer())).toString('base64'),
        email: data.get('email'),
        companies: allArr.name.map((name, index) => ({
            companyName: allArr.name[index],
            position: allArr.position[index],
            workedYears: allArr.workedYears[index],
            technologies: allArr.technologies[index],
        })).filter((company) => company.companyName && company.position && company.workedYears && company.technologies)
    }

    await client.sendEvent({
        name: 'create.resume',
        payload
    });

    return NextResponse.json({ })
}
```

> This code will run only with NodeJS version 20+. If you have a lower version, it will not be able to parse FormData.

That code is pretty simple.
- We parse the request as FormData using `req.formData`
- We convert the FormData-based request into a JSON file.
- We extract the image and convert it to `base64`
- We send everything to TriggerDev

<hr />

## Build the resume and send it to your email 📨

Building the resume is a long-running task we need to

- Use ChatGPT to generate the content.
- Create a PDF
- Send it to your email

We don't want to make a long-running HTTP request to make all of those for a few reasons.
1. When deploying to Vercel, there is a limit of 10 seconds on serverless functions. We will never make it on time.
2. We want to keep the user from hanging for a long time. It's a bad UX. If the user closes the window, the entire process will fail.

### Introducing Trigger.dev!

With Trigger.dev, you can run background processes inside of your NextJS app! You don't need to create a new server.
They also know how to handle long-running jobs by breaking them into short tasks seamlessly.

Sign up for a [Trigger.dev account](https://trigger.dev/). Once registered, create an organization and choose a project name for your job.


![CreateOrg](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shf1jsb4gio1zrjtz91d.jpeg)

Select Next.js as your framework and follow the process for adding Trigger.dev to an existing Next.js project.


![Next](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5guppb6rot13myu6th5c.jpeg)

Otherwise, click `Environments & API Keys` on the sidebar menu of your project dashboard.


![Copy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5gh527u7sthp6clkcfa.png)

Copy your DEV server API key and run the code snippet below to install Trigger.dev. Follow the instructions carefully.

```bash
npx @trigger.dev/cli@latest init
```

In another terminal, run the following code snippet to establish a tunnel between Trigger.dev and your Next.js project.

```bash
npx @trigger.dev/cli@latest dev
```

Let's create our TriggerDev job!
Head over to the newly created folder jobs and create a new file called `create.resume.ts`.

Add the following code:

```javascript
client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {
  }
});
```

This will create a new job for us called `create-resume`.
As you can see, there is a schema validation of the request we previously sent from our `route.tsx`. That will give us validation and also `auto-completion`.

We are going to run three jobs here
- ChatGPT
- Pdf creation
- Email sending

Let's start with ChatGPT.

[Create an OpenAI account](https://platform.openai.com/) and generate an API Key.

![ChatGPT](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ashau6i2sxcpd0qcxuwq.png)

Click `View API key` from the dropdown to create an API Key.

![ApiKey](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bzc6e7f7avemeuuaygr.png)

Next, install the OpenAI package by running the code snippet below.

```bash
npm install @trigger.dev/openai
```

Add your OpenAI API key to the `.env.local` file.

```bash
OPENAI_API_KEY=<your_api_key>
```

Create a new folder in the root directory called `utils`.
inside that directory, create a new file called `openai.ts`
Add the following code:

```javascript
import { OpenAI } from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
});

export async function generateResumeText(prompt: string) {
  const response = await openai.completions.create({
    model: "text-davinci-003",
    prompt,
    max_tokens: 250,
    temperature: 0.7,
    top_p: 1,
    frequency_penalty: 1,
    presence_penalty: 1,
  });

  return response.choices[0].text.trim();
}

export const prompts = {
  profileSummary: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technologies: ${knownTechnologies}. Can you write a 100 words description for the top of the resume(first person writing)?`,
  jobResponsibilities: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) =>  `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technolegies: ${knownTechnologies}. Can you write 3 points for a resume on what I am good at?`,
  workHistory: (fullName: string, currentPosition: string, workingExperience: string, details: TCompany[]) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). ${companyDetails(details)} \n Can you write me 50 words for each company seperated in numbers of my succession in the company (in first person)?`,
};
```

This code basically created the infrastructure to use ChatGPT and also 3 functions, `profileSummary`, `workingExperience`, and `workHistory`. We will use them to create the content for the sections.

Go back to our `create.resume.ts` and add the new job:

```javascript
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
import { prompts } from "@/utils/openai";
import { TCompany, TUserDetails } from "@/components/Home";

const companyDetails = (companies: TCompany[]) => {
  let stringText = "";
  for (let i = 1; i < companies.length; i++) {
    stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`;
  }
  return stringText;
};

client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  integrations: {
      resend
  },
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {

    const texts = await io.runTask("openai-task", async () => {
      return Promise.all([
          await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies))
      ]);
    });
  },
});
```

We created a new task called `openai-task`.
Inside that task, we simultaneously run three prompts with ChatGPT, and return them.

<hr />

## Creating the PDF

There are many ways to create a PDF
- You can use things like HTML2CANVAS and convert HTML code into an image and then a PDF.
- You can use things like `puppeteer` to scrape a web page and convert it to a PDF.
- You can use different libraries that can create PDFs on the backend side.

In our case, we are going to use a simple library called `jsPdf` it's very simplistic library to create PDF over the backend. I encourage you to create some more robust PDF files with Puppeteer and more HTML.

So let's install it
```bash
npm install jspdf @typs/jspdf --save
```

Let's return to `utils` and create a new file called `resume.ts`. That file will basically create a PDF file that we can send to the user's email.

Add the following content:
```javascript
import {TUserDetails} from "@/components/Home";
import {jsPDF} from "jspdf";

type ResumeProps = {
  userDetails: TUserDetails;
  picture: string;
  profileSummary: string;
  workHistory: string;
  jobResponsibilities: string;
};

export function createResume({ userDetails, picture, workHistory, jobResponsibilities, profileSummary }: ResumeProps) {
    const doc = new jsPDF();

    // Title block
    doc.setFontSize(24);
    doc.setFont('helvetica', 'bold');

    doc.text(userDetails.firstName + ' ' + userDetails.lastName, 45, 27);
    doc.setLineWidth(0.5);
    doc.rect(20, 15, 170, 20); // x, y, width, height
    doc.addImage({
        imageData: picture,
        x: 25,
        y: 17,
        width: 15,
        height: 15
    });

    // Reset font for the rest
    doc.setFontSize(12);
    doc.setFont('helvetica', 'normal');

    // Personal Information block
    doc.setFontSize(14);
    doc.setFont('helvetica', 'bold');
    doc.text('Summary', 20, 50);
    doc.setFontSize(10);
    doc.setFont('helvetica', 'normal');
    const splitText = doc.splitTextToSize(profileSummary, 170);
    doc.text(splitText, 20, 60);

    const newY = splitText.length * 5;

    // Work history block
    doc.setFontSize(14);
    doc.setFont('helvetica', 'bold');
    doc.text('Work History', 20, newY + 65);
    doc.setFontSize(10);
    doc.setFont('helvetica', 'normal');
    const splitWork = doc.splitTextToSize(workHistory, 170);
    doc.text(splitWork, 20, newY + 75);

    const newNewY = splitWork.length * 5;

    // Job Responsibilities block
    doc.setFontSize(14);
    doc.setFont('helvetica', 'bold');
    doc.text('Job Responsibilities', 20, newY + newNewY + 75);
    doc.setFontSize(10);
    doc.setFont('helvetica', 'normal');
    const splitJob = doc.splitTextToSize(jobResponsibilities, 170);
    doc.text(splitJob, 20, newY + newNewY + 85);

    return doc.output("datauristring");
}
```

This file contains three sections: `Personal Information`, `Work history`, and `Job Responsibilities` block.

We calculate where each block will be and what it will be.

Everything is set up in an `absolute` way.
A notable thing is the `splitTextToSize` to break the text into multiple lines, so it will not go off the screen.

![Resume](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdolng9e5ojev895x8i5.png)

Now, let's create the next task: open `resume.ts` again and add the following code:

```javascript
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
import { prompts } from "@/utils/openai";
import { TCompany, TUserDetails } from "@/components/Home";
import { createResume } from "@/utils/resume";

const companyDetails = (companies: TCompany[]) => {
  let stringText = "";
  for (let i = 1; i < companies.length; i++) {
    stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`;
  }
  return stringText;
};

client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  integrations: {
      resend
  },
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {

    const texts = await io.runTask("openai-task", async () => {
      return Promise.all([
          await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies))
      ]);
    });

    console.log('passed chatgpt');

    const pdf = await io.runTask('convert-to-html', async () => {
        const resume = createResume({
            userDetails: payload,
            picture: payload.photo,
            profileSummary: texts[0],
            jobResponsibilities: texts[1],
            workHistory: texts[2],
        });

        return {final: resume.split(',')[1]}
    });

    console.log('converted to pdf');
  },
});
```

You can see we have added a new task called `convert-to-html`. This will create the PDF for us, convert it to base64 and return it.

<hr />

## Let them know 🎤

We are reaching the end!
The only thing left is to share it with the user.
You can use any email service you want.
We will use Resend.com

Visit the [Signup page](https://resend.com/signup), create an account and an API Key, and save it into the `.env.local` file.

```bash
RESEND_API_KEY=<place_your_API_key>
```

![Key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yncrarbwcs65j44fs91y.png)

Install the [Trigger.dev Resend integration package](https://trigger.dev/docs/integrations/apis/resend) to your Next.js project.

```bash
npm install @trigger.dev/resend
```

All that is left to do is to add our last job!
Fortunately, Trigger directly integrates with Resend, so we don't need to create a new "normal" task.

Here is the final code:
```javascript
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
import { prompt } from "@/utils/openai";
import { TCompany, TUserDetails } from "@/components/Home";
import { createResume } from "@/utils/resume";
import { Resend } from "@trigger.dev/resend";

const resend = new Resend({
    id: "resend",
    apiKey: process.env.RESEND_API_KEY!,
});

const companyDetails = (companies: TCompany[]) => {
  let stringText = "";
  for (let i = 1; i < companies.length; i++) {
    stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`;
  }
  return stringText;
};

client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  integrations: {
      resend
  },
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {

    const texts = await io.runTask("openai-task", async () => {
      return Promise.all([
          await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies))
      ]);
    });

    console.log('passed chatgpt');

    const pdf = await io.runTask('convert-to-html', async () => {
        const resume = createResume({
            userDetails: payload,
            picture: payload.photo,
            profileSummary: texts[0],
            jobResponsibilities: texts[1],
            workHistory: texts[2],
        });

        return {final: resume.split(',')[1]}
    });

    console.log('converted to pdf');

    await io.resend.sendEmail('send-email', {
        to: payload.email,
        subject: 'Resume',
        html: 'Your resume is attached!',
        attachments: [
            {
                filename: 'resume.pdf',
                content: Buffer.from(pdf.final, 'base64'),
                contentType: 'application/pdf',
            }
        ],
        from: "Nevo David <nevo@gitup.dev>",
    });

    console.log('Sent email');
  },
});
```

We have the `Resend` instance at the top of our file loaded with our API key from the dashboard.

We have the
```
  integrations: {
      resend
  },
```

We added it to our job to use later inside of `io`.
And finally, we have our job to send the PDF `io.resend.sendEmail`

A notable thing is the attachment inside it with the PDF file we generated in the previous step.

And we are done 🎉


![We are done](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/esfhlds2qv1013c6x2h3.png)

You can check and run the full source code here:
https://github.com/triggerdotdev/blog


<hr />

## Let's connect! 🔌

As an open-source developer, you're invited to join our [community](https://discord.gg/nkqV9xBYWy) to contribute and engage with maintainers. Don't hesitate to visit our [GitHub repository](https://github.com/triggerdotdev/trigger.dev) to contribute and create issues related to Trigger.dev.

The source for this tutorial is available here:
https://github.com/triggerdotdev/blog/tree/main/blog-resume-builder

Thank you for reading!