<?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: Silas Ojugo</title>
    <description>The latest articles on DEV Community by Silas Ojugo (@ojugo007).</description>
    <link>https://dev.to/ojugo007</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3570934%2F8a8e7bde-8c17-4050-9a71-afd3b2505ad3.png</url>
      <title>DEV Community: Silas Ojugo</title>
      <link>https://dev.to/ojugo007</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ojugo007"/>
    <language>en</language>
    <item>
      <title>🚀 Building a Job Search Agent with Mastra, Google Gemini, and A2A Protocol</title>
      <dc:creator>Silas Ojugo</dc:creator>
      <pubDate>Sun, 09 Nov 2025 09:58:31 +0000</pubDate>
      <link>https://dev.to/ojugo007/building-a-job-search-agent-with-mastra-google-gemini-and-a2a-protocol-1gc1</link>
      <guid>https://dev.to/ojugo007/building-a-job-search-agent-with-mastra-google-gemini-and-a2a-protocol-1gc1</guid>
      <description>&lt;p&gt;AI agents are transforming intelligent automation, and Mastra is leading the way. In this post, I’ll show how I built the Findwork Agent — an AI that fetches real job listings and interacts naturally with users.&lt;br&gt;
We'll cover briefly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;What Mastra is and how it works&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Setting up your Google API key (via AI Studio)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Understanding the A2A Protocol and multi-agent interaction&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creating and deploying your agent to Mastra Cloud&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integrating your agent with Telex&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  🧠 &lt;strong&gt;What Is Mastra?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Mastra is an open-source framework for building intelligent AI agents that can reason, use external APIs, and communicate with others via the A2A protocol. Check out the documentation to learn more about &lt;a href="https://mastra.ai/docs/getting-started/installation" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt; and installation.&lt;br&gt;
In simpler terms, Mastra gives structure to AI workflows. Each agent has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A name&lt;/li&gt;
&lt;li&gt;Instructions (its role and tone)&lt;/li&gt;
&lt;li&gt;Tools (functions or APIs it can call)&lt;/li&gt;
&lt;li&gt;And an execution context where it processes data&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;🔐 Setting Up the Google API Key (from AI Studio)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To use Gemini models (like gemini-1.5-pro or gemini-2.0-flash), you’ll need a Google API key from AI Studio. You can enter it during Mastra setup or add it later in your project configuration.&lt;br&gt;
Steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Visit :&lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;ai studio&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Create API Key&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the key and store it safely&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to findwork.dev and generate your api key&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In your project’s root directory, create a .env file. This file is used to store environment variables such as API keys and secret tokens, which can be accessed in your code using process.env.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  🤝 &lt;strong&gt;Understanding the A2A Protocol&lt;/strong&gt;
&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.amazonaws.com%2Fuploads%2Farticles%2F145g7yjxi17wb6bv3xbr.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.amazonaws.com%2Fuploads%2Farticles%2F145g7yjxi17wb6bv3xbr.jpg" alt=" " width="576" height="321"&gt;&lt;/a&gt;&lt;br&gt;
Mastra implements the A2A (Agent-to-Agent) protocol, which allows multiple agents to collaborate intelligently.&lt;br&gt;
For example, in our setup:&lt;br&gt;
The Findwork Agent focuses on finding job listings via the Findwork API.&lt;br&gt;
Another agent (like a Career Assistant) could generate a cover letter based on those listings.&lt;/p&gt;

&lt;p&gt;These agents can pass structured data to one another — e.g., one agent fetches data, and the other interprets it.&lt;br&gt;
This decoupled structure keeps your AI system modular, scalable, and reusable.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧩 &lt;strong&gt;Creating the Findwork Tool&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Findwork API provides access to real-time job listings.&lt;br&gt;
Below is the Mastra tool that powers our agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

interface JobResponse {
  count: number;
  results: {
    role: string;
    company_name: string;
    company_num_employees: string | number | null;
    employment_type: string | null;
    location: string;
    remote: boolean;
    url: string;
    text: string;
  }[];
}

export const jobTool = createTool({
  id: 'get-job',
  description: 'Fetch relevant job postings from Findwork based on location, skills, and remote preference.',

  inputSchema: z.object({
    location: z.string().optional(),
    skills: z.string(),
    remote: z.boolean().optional()
  }),

  outputSchema: z.object({
    count: z.number(),
    results: z.array(z.object({
      role: z.string(),
      company_name: z.string(),
      company_num_employees: z.union([z.string(), z.number(), z.null()]),
      employment_type: z.string().nullable(),
      location: z.string(),
      remote: z.boolean(),
      url: z.string(),
      text: z.string(),
    })),
  }),

  execute: async ({ input }) =&amp;gt; {
    const { location, skills, remote } = input;
    const params = new URLSearchParams();
    if (skills) params.append('search', skills);
    if (location) params.append('location', location);
    if (remote) params.append('remote', 'true');

    const url = `https://findwork.dev/api/jobs/?${params.toString()}`;

    const jobResponse = await fetch(url, {
      headers: {
        Authorization: `Token ${process.env.FINDWORK_API_KEY}`,
      },
    });

    if (!jobResponse.ok) throw new Error('Error fetching jobs: ' + jobResponse.statusText);

    return (await jobResponse.json()) as JobResponse;
  },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;🧑‍💻 Creating the Findwork Agent&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now that we have a tool, let’s create our agent that uses it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
import { jobTool } from '../tools/jobber-tool'
import { scorers } from '../scorers/weather-scorer';


export const findworkAgent = new Agent({
    name: 'Findwork Agent',
    instructions: `
        You are a professional job search assistant who helps users find relevant and recent job opportunities.

        **Core Responsibilities:**
        - Use the jobTool to search for jobs based on the user's query.
        - Understand the user's input to determine:
        - The job title or key skills they are interested in.
        - The preferred job location (if mentioned).
        - Whether they are looking for remote positions.

        **Behavior Guidelines:**
        - Always call the jobTool with the correct parameters: location, skills, and remote.
        - If the user does not specify a location, default to remote job searches.
        - If neither skills nor location are provided, politely ask the user for more details.
        - Present job results clearly — include role, company name, location, remote status, and URL.
        - Keep responses concise and professional.

        **Tone:**
        - Be helpful, direct, and conversational.
        - Avoid unnecessary filler text; focus on delivering useful job results quickly.

        **Tools:**
        - Use only the jobTool to fetch job listings from Findwork.
    `,
    model: 'google/gemini-2.5-pro',
    tools: { jobTool },
    scorers: {
        toolCallAppropriateness: {
          scorer: scorers.toolCallAppropriatenessScorer,
          sampling: {
            type: 'ratio',
            rate: 1,
          },
        },
        completeness: {
          scorer: scorers.completenessScorer,
          sampling: {
            type: 'ratio',
            rate: 1,
          },
        },
        translation: {
          scorer: scorers.translationScorer,
          sampling: {
            type: 'ratio',
            rate: 1,
          },
        },
      },

    memory: new Memory({
        storage: new LibSQLStore({
            url: 'file:../mastra.db',
        }),
    }),
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;↘️ Creating A2A route Handler&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The key component connecting Mastra agents to the A2A protocol is a custom route handler. It ensures agent responses are properly formatted in A2A structure, while also managing artifacts and maintaining conversation history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { registerApiRoute } from '@mastra/core/server';
import { randomUUID } from 'crypto';


type MessagePart =
  | { kind: 'text'; text: string }
  | { kind: 'data'; data: unknown };

type A2AMessage = {
  role: string;
  parts?: MessagePart[];
  messageId?: string;
  taskId?: string;
};

export const a2aAgentRoute = registerApiRoute('/a2a/agent/:agentId', {
  method: 'POST',
  handler: async (c) =&amp;gt; {
    try {
      const mastra = c.get('mastra');
      const agentId = c.req.param('agentId');

      // Parse JSON-RPC 2.0 request
      const body = await c.req.json();
      const { jsonrpc, id: requestId, method, params } = body;

      // Validate JSON-RPC 2.0 format
      if (jsonrpc !== '2.0' || !requestId) {
        return c.json({
          jsonrpc: '2.0',
          id: requestId || null,
          error: {
            code: -32600,
            message: 'Invalid Request: jsonrpc must be "2.0" and id is required'
          }
        }, 400);
      }

      const agent = mastra.getAgent(agentId);
      if (!agent) {
        return c.json({
          jsonrpc: '2.0',
          id: requestId,
          error: {
            code: -32602,
            message: `Agent '${agentId}' not found`
          }
        }, 404);
      }

      // Extract messages from params
      const { message, messages, contextId, taskId, metadata } = params || {};

      let messagesList = [];
      if (message) {
        messagesList = [message];
      } else if (messages &amp;amp;&amp;amp; Array.isArray(messages)) {
        messagesList = messages;
      }

      // Convert A2A messages to Mastra format
      const mastraMessages = messagesList.map((msg) =&amp;gt; ({
        role: msg.role,
        content: msg.parts?.map((part:MessagePart) =&amp;gt; {
          if (part.kind === 'text') return part.text;
          if (part.kind === 'data') return JSON.stringify(part.data);
          return '';
        }).join('\n') || ''
      }));

      // Execute agent
      const response = await agent.generate(mastraMessages);
      const agentText = response.text || '';

      // Build artifacts array
      const artifacts:any = [
        {
          artifactId: randomUUID(),
          name: `${agentId}Response`,
          parts: [{ kind: 'text', text: agentText }]
        }
      ];

      // Add tool results as artifacts
      if (response.toolResults &amp;amp;&amp;amp; response.toolResults.length &amp;gt; 0) {
        artifacts.push({
          artifactId: randomUUID(),
          name: 'ToolResults',
          parts: response.toolResults.map((result) =&amp;gt; ({
            kind: 'text',
            text: JSON.stringify(result)
          }))
        });
      }

      // Build conversation history
      const history = [
        ...messagesList.map((msg) =&amp;gt; ({
          kind: 'message',
          role: msg.role,
          parts: msg.parts,
          messageId: msg.messageId || randomUUID(),
          taskId: msg.taskId || taskId || randomUUID(),
        })),
        {
          kind: 'message',
          role: 'agent',
          parts: [{ kind: 'text', text: agentText }],
          messageId: randomUUID(),
          taskId: taskId || randomUUID(),
        }
      ];

      // Return A2A-compliant response
      return c.json({
        jsonrpc: '2.0',
        id: requestId,
        result: {
          id: taskId || randomUUID(),
          contextId: contextId || randomUUID(),
          status: {
            state: 'completed',
            timestamp: new Date().toISOString(),
            message: {
              messageId: randomUUID(),
              role: 'agent',
              parts: [{ kind: 'text', text: agentText }],
              kind: 'message'
            }
          },
          artifacts,
          history,
          kind: 'task'
        }
      });

    } catch (error:any) {
      return c.json({
        jsonrpc: '2.0',
        id: null,
        error: {
          code: -32603,
          message: 'Internal error',
          data: { details: error.message || String(error) }
        }
      }, 500);
    }
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;✍️ Registering our agent and custom route with mastra&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We need to let Mastra know about our agent and our custom route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Mastra } from '@mastra/core/mastra';
import { PinoLogger } from '@mastra/loggers';
import { LibSQLStore } from '@mastra/libsql';

import { findworkAgent } from './agents/findwork-agent';
import { a2aAgentRoute } from './routes/a2a-route';

export const mastra = new Mastra({
  workflows: { weatherWorkflow },
  agents: {findworkAgent },
  server: {
    apiRoutes: [a2aAgentRoute]
  },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),

});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;🧪 Testing with Postman&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now that your agent is deployed, test it with Postman&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;post request:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "jsonrpc": "2.0",
  "id": "request-001",
  "method": "message/send",
  "params": {
    "message": {
      "kind": "message",
      "role": "user",
      "parts": [
        {
          "kind": "text",
          "text": "i need a job in london with react skill in high demand?"
        }
      ],
      "messageId": "msg-001",
      "taskId": "task-001"
    },
    "configuration": {
      "blocking": true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;response from agent:&lt;/strong&gt;&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F8kq5vo96wsez33h7wbzs.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.amazonaws.com%2Fuploads%2Farticles%2F8kq5vo96wsez33h7wbzs.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Integration with Telex&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Telex.im allows you to deploy your Mastra agent as a co-worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create an AI Co-Worker in Telex&lt;/strong&gt;&lt;br&gt;
In your Telex dashboard, navigate to the AI Co-Workers section and create a new co-worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Define Your Workflow&lt;/strong&gt;&lt;br&gt;
In the workflow editor, paste the following workflow definition. The key component here is the node definition, which tells Telex how to communicate with your Mastra agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "active": false,
  "category": "utilities",
  "description": "A workflow that gets job posted findwork",
  "id": "sGC3u7y4vBaZww0G",
  "name": "findwork agent",
  "long_description": "You are a professional job search assistant who helps users find relevant and recent job opportunities. Core Responsibilities: Use the jobTool to search for jobs based on the user's query. - Understand the user's input to determine:  - The job title or key skills they are interested in - The preferred job location (if mentioned).  - Whether they are looking for remote positions. Behavior Guidelines: - Always call the jobTool with the correct parameters: location, skills, and remote. - If the user does not specify a location, default to remote job searches. - If neither skills nor location are provided, politely ask the user for more details. - Present job results clearly — include role, company name, location, remote status, and URL. - Keep responses concise and professional. Tone: - Be helpful, direct, and conversational. - Avoid unnecessary filler text; focus on delivering useful job results quickly. Tools: - Use only the jobTool to fetch job listings from Findwork.",
  "short_description": "Search for jobs by location, skill, and remote status using the Findwork API.",
  "nodes": [
    {
      "id": "findwork_agent",
      "name": "findwork agent",
      "parameters": {},
      "position": [816, -112],
      "type": "a2a/mastra-a2a-node",
      "typeVersion": 1,
      "url": "https://findwork-agent.mastra.cloud/a2a/agent/findworkAgent"
    }
  ],
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can start exploring jobs that match your skills and interests! 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🤖 How it works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When a user interacts with your Telex workflow, it sends an A2A request — just like the POST request body we tested earlier in Postman. Mastra then receives this request, extracts the user’s query, uses the appropriate tool to fetch the requested data, and generates a clear, user-friendly response. Isn’t that beautiful? ✨&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🥳Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;By combining Mastra, Gemini, and the Findwork API, we built an intelligent job-finding agent that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Understands natural language queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fetches live job listings&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Can be extended with other AI agents (like cover letter or interview coaches)&lt;br&gt;
This is the power of A2A architecture — modular, scalable, and intelligent.&lt;/p&gt;

</description>
      <category>mastra</category>
      <category>gemini</category>
      <category>webdev</category>
      <category>a2a</category>
    </item>
  </channel>
</rss>
