<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ai-Assistant on Damian Galarza | Software Engineering &amp; AI Consulting</title><link>https://www.damiangalarza.com/tags/ai-assistant/</link><description>Recent posts from Damian Galarza | Software Engineering &amp; AI Consulting</description><generator>Hugo</generator><language>en-us</language><managingEditor>Damian Galarza</managingEditor><atom:link href="https://www.damiangalarza.com/tags/ai-assistant/feed.xml" rel="self" type="application/rss+xml"/><item><title>How I Built a Personal AI Assistant with Mastra</title><link>https://www.damiangalarza.com/posts/2026-03-06-build-personal-ai-assistant/</link><pubDate>Fri, 06 Mar 2026 00:00:00 -0500</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/posts/2026-03-06-build-personal-ai-assistant/</guid><description>A practical guide to building an AI agent with Mastra that researches contacts, schedules follow-ups, integrates with Slack, and uses layered memory.</description><content:encoded><![CDATA[<p>Most &ldquo;AI assistants&rdquo; are just chatbots with a context window. Ask them something, get an answer. Ask again later, and they have no idea who you are.</p>
<p>That&rsquo;s not an assistant. That&rsquo;s a search engine with a personality.</p>
<p>I wanted something different. I wanted an agent that:</p>
<ul>
<li>Researches people before my meetings</li>
<li>Reminds me to follow up</li>
<li>Remembers context across conversations</li>
<li>Acts on its own when events happen</li>
</ul>
<p>So I built one. Here&rsquo;s how it works.</p>
<h2 id="the-goal">The Goal</h2>
<p>I meet with a lot of people: founders, potential clients, partners. Before each meeting, I want to know who I&rsquo;m talking to: their background, their company, what they&rsquo;ve been working on. After each meeting, I follow up at the right time.</p>
<p>Doing this manually doesn&rsquo;t scale. I needed an agent that handles it for me.</p>
<h2 id="architecture-overview">Architecture Overview</h2>
<p>The system has five main components:</p>
<ol>
<li><strong>Communication</strong>: Slack as the primary interface, built on a platform-agnostic SDK</li>
<li><strong>Tools</strong>: Composable functions for research, scheduling, and data retrieval</li>
<li><strong>Memory</strong>: Four types (message history, semantic recall, working memory, observational) so the agent remembers across conversations</li>
<li><strong>Webhooks</strong>: Event-driven triggers that let the agent react to Cal.com bookings automatically</li>
<li><strong>Task Scheduling</strong>: Time-delayed task execution for follow-ups and reminders</li>
</ol>
<p>Each component does one thing. Together, they form an agent that acts, remembers, and follows up.</p>
<h2 id="1-tools-what-the-agent-can-do">1. Tools: What the Agent Can Do</h2>
<p>Tools are the agent&rsquo;s capabilities. Each tool is a function the agent can call when it decides to use it.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/mastra/tools/cal-com.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { createTool } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core/tools&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { z } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;zod&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> getUpcomingEvents <span style="color:#89dceb;font-weight:bold">=</span> createTool({
</span></span><span style="display:flex;"><span>  id<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;getUpcomingEvents&#34;</span>,
</span></span><span style="display:flex;"><span>  description<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;Get upcoming meetings from Cal.com for the specified date range&#34;</span>,
</span></span><span style="display:flex;"><span>  inputSchema: <span style="color:#f38ba8">z.object</span>({
</span></span><span style="display:flex;"><span>    startDate: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    endDate: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>  outputSchema: <span style="color:#f38ba8">z.object</span>({
</span></span><span style="display:flex;"><span>    title: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    startTime: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    endTime: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    attendees: <span style="color:#f38ba8">z.array</span>(z.<span style="color:#f38ba8">string</span>()),
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>  execute: <span style="color:#f38ba8">async</span> ({ startDate, endDate }) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">const</span> events <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> fetchCalComEvents(startDate, endDate);
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">return</span> events.map((e) <span style="color:#89dceb;font-weight:bold">=&gt;</span> ({
</span></span><span style="display:flex;"><span>      title: <span style="color:#f38ba8">e.title</span>,
</span></span><span style="display:flex;"><span>      startTime: <span style="color:#f38ba8">e.start</span>,
</span></span><span style="display:flex;"><span>      endTime: <span style="color:#f38ba8">e.end</span>,
</span></span><span style="display:flex;"><span>      attendees: <span style="color:#f38ba8">e.attendees.map</span>((a) <span style="color:#89dceb;font-weight:bold">=&gt;</span> a.email),
</span></span><span style="display:flex;"><span>    }));
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>The tool description is the contract. The agent reads the description and decides when to call the tool. Clear descriptions = reliable tool usage.</p>
<p>I defined tools for Cal.com (scheduling), Exa (web research), and Slack (messaging):</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>getUpcomingEvents</code></td>
          <td>Fetch meetings from Cal.com</td>
      </tr>
      <tr>
          <td><code>searchVault</code></td>
          <td>Search my contacts and notes</td>
      </tr>
      <tr>
          <td><code>researchPerson</code></td>
          <td>Research a person/company via Exa</td>
      </tr>
      <tr>
          <td><code>postToSlack</code></td>
          <td>Post to the assistant channel</td>
      </tr>
      <tr>
          <td><code>scheduleTask</code></td>
          <td>Schedule a follow-up task</td>
      </tr>
  </tbody>
</table>
<p>Each tool does one thing. Simple, composable functions the agent can combine.</p>
<h2 id="2-memory-the-differentiator">2. Memory: The Differentiator</h2>
<p>Most agents have no memory. Ask them something, they answer. Ask again later, they start fresh.</p>
<p>In <a href="/posts/2026-02-17-how-ai-agents-remember-things/">How AI Agents Remember Things</a>, I covered the conceptual taxonomy: episodic memory for events and interactions, semantic memory for stable facts and preferences. Mastra maps those concepts to four concrete memory types you configure when building an agent.</p>
<table>
  <thead>
      <tr>
          <th>Mastra Type</th>
          <th>What it does</th>
          <th>Conceptual equivalent</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Message history</strong></td>
          <td>Keeps recent messages in context within a conversation</td>
          <td>Episodic (in-session)</td>
      </tr>
      <tr>
          <td><strong>Semantic recall</strong></td>
          <td>Retrieves relevant messages from past conversations by meaning</td>
          <td>Episodic (cross-session)</td>
      </tr>
      <tr>
          <td><strong>Working memory</strong></td>
          <td>Persistent structured data: your name, preferences, goals</td>
          <td>Semantic (stable facts)</td>
      </tr>
      <tr>
          <td><strong>Observational memory</strong></td>
          <td>Background summarization to keep the context window small over time</td>
          <td>Session compaction</td>
      </tr>
  </tbody>
</table>
<p><strong>Message history</strong> (<code>lastMessages</code>) is the short-term layer. It keeps the last N messages in context so the agent can follow the conversation thread. The agent can reference something you said three messages ago without you repeating it. Ten messages works well for most conversational flows.</p>
<p><strong>Semantic recall</strong> is the long-term retrieval layer. It uses vector embeddings to search across all past conversations by meaning, not keywords. When you say &ldquo;remember that thing about the Cal.com integration,&rdquo; the agent encodes your query into a vector and finds the closest matches from past messages. You configure <code>topK</code> (how many matches to retrieve) and <code>messageRange</code> (how many surrounding messages to include for context). I used LibSQL for the vector store and FastEmbed for local embeddings, so the entire pipeline runs without external API calls.</p>
<p><strong>Working memory</strong> is the persistent layer. It&rsquo;s a structured scratchpad the agent updates over time as it learns about you. Unlike message history and semantic recall, which store raw messages, working memory stores distilled facts: your name, your role, your preferences. You define a template, and the agent fills it in as it picks up information from conversations. This is what makes the agent feel like it knows you, even in a brand new thread.</p>
<p><strong>Observational memory</strong> uses background Observer and Reflector agents to maintain a dense observation log that replaces raw message history as it grows. I haven&rsquo;t wired this up yet, but it solves the context window problem: as conversations get long, you can&rsquo;t keep everything in context. Observational memory compresses it down without losing the long-term thread.</p>
<p>Here&rsquo;s how the first three are configured:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/mastra/agents/meeting-assistant.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { Agent } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/core/agent&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { Memory } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/memory&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { LibSQLVector } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/libsql&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { fastembed } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/fastembed&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">const</span> memory <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Memory({
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic">// Vector store for semantic recall
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  vector: <span style="color:#f38ba8">new</span> LibSQLVector({
</span></span><span style="display:flex;"><span>    id<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;memory-vector&#34;</span>,
</span></span><span style="display:flex;"><span>    url<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;file:./mastra.db&#34;</span>,
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic">// Local embedding model, no API key needed
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  embedder: <span style="color:#f38ba8">fastembed</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  options<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Message history: keeps the last 10 messages in context
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    lastMessages: <span style="color:#f38ba8">10</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Semantic recall: searches past conversations by meaning
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    semanticRecall<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      topK: <span style="color:#f38ba8">3</span>,
</span></span><span style="display:flex;"><span>      messageRange: <span style="color:#f38ba8">2</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Working memory: persistent user profile the agent updates over time
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    workingMemory<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      enabled: <span style="color:#f38ba8">true</span>,
</span></span><span style="display:flex;"><span>      template<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">`# User Profile
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Name:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Role:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Company:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Communication style:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Meeting prep preferences:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">`</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> meetingAssistant <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Agent({
</span></span><span style="display:flex;"><span>  name<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#39;MeetingAssistant&#39;</span>,
</span></span><span style="display:flex;"><span>  model<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#39;openai/gpt-4.1&#39;</span>,
</span></span><span style="display:flex;"><span>  memory,
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>Mastra handles storage, retrieval, and injection into the agent&rsquo;s context automatically. You configure the types declaratively; the framework does the rest.</p>
<h2 id="3-slack-integration">3. Slack Integration</h2>
<p>Communication happens through Slack via the <a href="https://chat-sdk.dev/">Chat SDK</a>, a platform-agnostic interface for bot communication.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/chat.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { Chat } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;chat&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { createSlackAdapter } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@chat-adapter/slack&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { meetingAssistant } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;./mastra/agents/meeting-assistant&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> bot <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Chat({
</span></span><span style="display:flex;"><span>  userName<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;meeting-assistant&#34;</span>,
</span></span><span style="display:flex;"><span>  adapters<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    slack: <span style="color:#f38ba8">createSlackAdapter</span>(),
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>bot.onNewMention(<span style="color:#cba6f7">async</span> (thread, message) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> thread.subscribe();
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> thread.startTyping();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">const</span> result <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> meetingAssistant.generate(message.text, {
</span></span><span style="display:flex;"><span>    memory<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      thread: <span style="color:#f38ba8">thread.id</span>,
</span></span><span style="display:flex;"><span>      resource<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> thread.post(result.text);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>Two things happening here. First, <code>onNewMention</code> fires when someone @mentions the bot in Slack. Second, <code>memory.thread</code> scopes messages to the specific Slack thread, while <code>memory.resource</code> uses a fixed ID so working memory (your profile) is shared across all threads.</p>
<h2 id="4-webhooks-reacting-to-events">4. Webhooks: Reacting to Events</h2>
<p>The agent needs to know when a meeting is booked. That&rsquo;s where webhooks come in.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/mastra/index.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { Mastra } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { registerApiRoute } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core/server&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> mastra <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Mastra({
</span></span><span style="display:flex;"><span>  server<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    apiRoutes<span style="color:#89dceb;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>      registerApiRoute(<span style="color:#a6e3a1">&#34;/webhooks/cal&#34;</span>, {
</span></span><span style="display:flex;"><span>        method<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;POST&#34;</span>,
</span></span><span style="display:flex;"><span>        handler: <span style="color:#f38ba8">async</span> (c) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> payload <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> c.req.json();
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> triggerEvent <span style="color:#89dceb;font-weight:bold">=</span> payload.triggerEvent;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">if</span> (triggerEvent <span style="color:#89dceb;font-weight:bold">!==</span> <span style="color:#a6e3a1">&#34;BOOKING_CREATED&#34;</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">return</span> c.json({ ok: <span style="color:#f38ba8">true</span>, skipped: <span style="color:#f38ba8">true</span> });
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> attendee <span style="color:#89dceb;font-weight:bold">=</span> payload.payload<span style="color:#89dceb;font-weight:bold">?</span>.attendees<span style="color:#89dceb;font-weight:bold">?</span>.[<span style="color:#fab387">0</span>];
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> channel <span style="color:#89dceb;font-weight:bold">=</span> bot.channel(<span style="color:#a6e3a1">`slack:</span><span style="color:#a6e3a1">${</span>process.env.SLACK_CHANNEL_ID<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#6c7086;font-style:italic">// Post immediately, then research asynchronously
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>          channel.post(<span style="color:#a6e3a1">`Researching *</span><span style="color:#a6e3a1">${</span>attendee.name<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">* for upcoming meeting...`</span>).then(<span style="color:#cba6f7">async</span> (sent) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">const</span> threadId <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">`slack:</span><span style="color:#a6e3a1">${</span>channelId<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">:</span><span style="color:#a6e3a1">${</span>sent.id<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">const</span> prompt <span style="color:#89dceb;font-weight:bold">=</span> [
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`I have a meeting coming up with </span><span style="color:#a6e3a1">${</span>attendee.name<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1"> (</span><span style="color:#a6e3a1">${</span>attendee.email<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">).`</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`Event: </span><span style="color:#a6e3a1">${</span>payload.payload.title<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`Time: </span><span style="color:#a6e3a1">${</span>payload.payload.startTime<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`Research this person and give me a concise meeting brief.`</span>,
</span></span><span style="display:flex;"><span>            ].join(<span style="color:#a6e3a1">&#34;\n&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">const</span> result <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> meetingAssistant.generate(prompt);
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">await</span> slack.postMessage(threadId, { markdown: <span style="color:#f38ba8">result.text</span> });
</span></span><span style="display:flex;"><span>          });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">return</span> c.json({ ok: <span style="color:#f38ba8">true</span> });
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>      }),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>The webhook receives the Cal.com booking payload, immediately posts a &ldquo;researching&rdquo; message to Slack, then kicks off the agent to research and post the brief. This way Cal.com doesn&rsquo;t time out waiting for the research to complete.</p>
<h2 id="5-task-scheduling-time-delayed-actions">5. Task Scheduling: Time-Delayed Actions</h2>
<p>After a meeting, the agent should follow up. That requires scheduling a task for later execution.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/scheduler.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">async</span> <span style="color:#f38ba8">function</span> scheduleTask(
</span></span><span style="display:flex;"><span>  name: <span style="color:#f38ba8">string</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">type</span><span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#f38ba8">string</span>,
</span></span><span style="display:flex;"><span>  scheduledFor: <span style="color:#f38ba8">string</span>,
</span></span><span style="display:flex;"><span>  payload: <span style="color:#f38ba8">Record</span>&lt;<span style="color:#cba6f7">string</span>, <span style="color:#89b4fa">unknown</span>&gt;,
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> db.insert(scheduledTasks).values({
</span></span><span style="display:flex;"><span>    name,
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">type</span>,
</span></span><span style="display:flex;"><span>    scheduledFor,
</span></span><span style="display:flex;"><span>    payload: <span style="color:#f38ba8">JSON.stringify</span>(payload),
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The scheduler polls every 30 seconds for due tasks, marks them as running, executes the handler, then marks them complete or failed. Simple, reliable, no external dependencies.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>registerTaskHandler(<span style="color:#a6e3a1">&#34;follow-up&#34;</span>, <span style="color:#cba6f7">async</span> (payload) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">const</span> { threadId, message } <span style="color:#89dceb;font-weight:bold">=</span> payload <span style="color:#cba6f7">as</span> { threadId: <span style="color:#f38ba8">string</span>; message: <span style="color:#f38ba8">string</span> };
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">const</span> slack <span style="color:#89dceb;font-weight:bold">=</span> bot.getAdapter(<span style="color:#a6e3a1">&#34;slack&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> slack.postMessage(threadId, { markdown: <span style="color:#f38ba8">message</span> });
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>When the meeting ends, the webhook handler schedules a follow-up task. Thirty seconds after the scheduled end time, the handler fires and posts to the Slack thread: &ldquo;The meeting should be wrapping up! How did it go?&rdquo;</p>
<h2 id="the-mental-model">The Mental Model</h2>
<p>Here&rsquo;s how I think about agent architecture now:</p>
<p><strong>Communication</strong> is the interface. Slack, Telegram, or any messaging platform. It&rsquo;s just how the user talks to the agent.</p>
<p><strong>Tools</strong> are the agent&rsquo;s capabilities. Each tool should do one thing well. The description is the contract. Write it clearly, or the agent won&rsquo;t know when to use it.</p>
<p><strong>Memory</strong> is the differentiator. Message history for in-session continuity, semantic recall for cross-session retrieval, working memory for persistent user facts. Most agents fail because they only implement one or none.</p>
<p><strong>Webhooks</strong> make it proactive. Without external triggers, the agent only acts when asked. With webhooks, it can act on events: bookings, form submissions, anything.</p>
<p><strong>Task scheduling</strong> closes the loop. The agent doesn&rsquo;t just respond. It reminds, follows up, checks in. Time-delayed actions turn a reactive chatbot into a proactive assistant.</p>
<h2 id="the-point">The Point</h2>
<p>The point isn&rsquo;t the code. It&rsquo;s the architecture.</p>
<p>An agent that only chats doesn&rsquo;t get you very far. An agent that integrates with your tools, remembers across conversations, reacts to events, and follows up on time is actually useful.</p>
<p>Mastra makes this easier than rolling your own. But the pattern works with any framework: give the agent tools, give it memory in layers, connect it to events, and let it act on time.</p>
<p>That&rsquo;s an assistant. Everything else is a chatbot.</p>
<h2 id="code">Code</h2>
<p>The full implementation is on GitHub: <a href="https://github.com/dgalarza/mastra-meeting-assistant">github.com/dgalarza/mastra-meeting-assistant</a></p>
<h2 id="want-help-building-this">Want Help Building This?</h2>
<p>If you&rsquo;re building AI agents into your workflow, whether it&rsquo;s a personal assistant, an internal tool, or a customer-facing product, <a href="/ai-agents">I can help</a>.</p>
<h2 id="further-reading">Further Reading</h2>
<ul>
<li><a href="https://mastra.ai/docs/">Mastra Documentation</a></li>
<li><a href="https://mastra.ai/docs/memory/overview">Mastra Memory</a></li>
<li><a href="https://chat-sdk.dev/">Chat SDK</a></li>
<li><a href="/posts/2026-02-17-how-ai-agents-remember-things/">How AI Agents Remember Things</a>: deep dive into the memory taxonomy and architecture behind agents that remember</li>
<li><a href="https://youtu.be/Seu7nksZ_4k?si=Xx8wnlL6j8nsLYq5">How AI Agents Remember Things (YouTube)</a>: video walkthrough of agent memory systems</li>
<li><a href="https://docs.exa.ai/">Exa API</a></li>
</ul>
]]></content:encoded></item></channel></rss>