<?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: Inngest</title>
    <description>The latest articles on DEV Community by Inngest (@inngest).</description>
    <link>https://dev.to/inngest</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%2Forganization%2Fprofile_image%2F8590%2F4b1e785e-5ce1-4e75-a78e-13ecc5afab91.png</url>
      <title>DEV Community: Inngest</title>
      <link>https://dev.to/inngest</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/inngest"/>
    <language>en</language>
    <item>
      <title>Replacing complex UX patterns with Next.js and OpenAI o1</title>
      <dc:creator>Charly Poly</dc:creator>
      <pubDate>Thu, 17 Oct 2024 16:02:39 +0000</pubDate>
      <link>https://dev.to/inngest/replacing-complex-ux-patterns-with-nextjs-and-openai-o1-7pi</link>
      <guid>https://dev.to/inngest/replacing-complex-ux-patterns-with-nextjs-and-openai-o1-7pi</guid>
      <description>&lt;p&gt;Anyone who has already built a data-import user experience knows it requires some complex column-matching UI and many branches on the backend to safely parse and save the user-provided data:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F9mrnee1ly9rsvb9t7i4z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F9mrnee1ly9rsvb9t7i4z.png" alt="Image description" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Such UI is a slow and tedious experience for the end-user and a challenge to maintain for developers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this article, we will review a demo CRM Next.js putting a contacts import feature on autopilot, remarkably demonstrating the power of agentic workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an AI Agentic workflow?
&lt;/h2&gt;

&lt;p&gt;An Agentic workflow is a new pattern consisting of relying on model reasoning to achieve a goal instead of providing a series of static prompts.&lt;br&gt;
In short, the model is given a high-level goal to achieve and figures out the necessary steps to reach that goal.&lt;/p&gt;

&lt;p&gt;Many &lt;a href="https://arxiv.org/abs/2210.03629" rel="noopener noreferrer"&gt;papers&lt;/a&gt; and &lt;a href="https://www.techzine.eu/blogs/applications/118176/the-ai-shift-from-prompt-engineering-to-flow-engineering/" rel="noopener noreferrer"&gt;articles&lt;/a&gt; have been published earlier this year, showcasing the &lt;a href="https://www.deeplearning.ai/the-batch/how-agents-can-improve-llm-performance/" rel="noopener noreferrer"&gt;outstanding performance of agentic workflows&lt;/a&gt; compared to standard prompting techniques.&lt;br&gt;&lt;br&gt;
It turns out that agentic workflows, by combining reflection (reasoning), planning, and tools, lead to more precise answers, &lt;a href="https://www.bbc.com/travel/article/20240222-air-canada-chatbot-misinformation-what-travellers-should-know" rel="noopener noreferrer"&gt;solving important integration issues&lt;/a&gt; of AI in user-facing use cases.&lt;/p&gt;

&lt;p&gt;We are now witnessing the rise of new tools that help with agentic workflow developments with LangGraph and, recently, &lt;a href="https://github.com/openai/swarm" rel="noopener noreferrer"&gt;OpenAI's swarm&lt;/a&gt;, along with their new reasoning model: &lt;a href="https://openai.com/o1/" rel="noopener noreferrer"&gt;&lt;code&gt;o1&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
Adding Agentic workflows to an application comes with many challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duration volatility&lt;/strong&gt;: Models are responsible for reflecting and planning all the steps to reach a given goal, resulting in significant variance in the workflow duration.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: Agentic workflows rely on more tools than regular prompt-designed workflows. Adding more tools calling external APIs based on LLM-generated parameters raises the risk of failure. Recovering from failure enables the model to self-correct during the workflow execution.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unpredictability&lt;/strong&gt;: Pushing an agentic workflow to production requires setting up some safeguards on cost, duration, and output validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's now look at our Next.js CRM demo, featuring an autopilot contacts import feature.&lt;br&gt;&lt;br&gt;
We'll see that agentic workflows mostly reuse existing patterns found in Next.js  and can already be used today to bring brand-new capabilities to Next.js applications.&lt;/p&gt;
&lt;h2&gt;
  
  
  Designing an agentic workflow with OpenAI o1
&lt;/h2&gt;

&lt;p&gt;We will design our CRM's autopilot contact importer feature by leveraging the new OpenAI &lt;code&gt;o1&lt;/code&gt; reasoning model to plan the best steps to import a given file by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensuring that the fields are properly set, without typos
&lt;/li&gt;
&lt;li&gt;Ranking the provided contact as good software Sales lead dynamically based on the provided columns
&lt;/li&gt;
&lt;li&gt;Matching provided column by our CRM ones: Name, Email, Role, Company&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open AI &lt;code&gt;o1&lt;/code&gt; will take this high-level goal to generate a set of steps leveraging our existing handwritten actions (&lt;code&gt;convert&lt;/code&gt;, &lt;code&gt;enrich&lt;/code&gt;, &lt;code&gt;save&lt;/code&gt;) and generating custom OpenAI calls (if necessary) while managing cost.&lt;/p&gt;

&lt;p&gt;Here's a visualization of our autopilot import contacts feature in our Next.js application:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Foz57h2wokxjuinhd62kq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Foz57h2wokxjuinhd62kq.jpg" alt="Image description" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating our contacts importer UI with Vercel v0
&lt;/h2&gt;

&lt;p&gt;Our import contacts UI has been, of course, generated with &lt;a href="https://v0.dev/" rel="noopener noreferrer"&gt;Vercel v0&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F83sfdpysltr1s7odpwy0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F83sfdpysltr1s7odpwy0.png" alt="Image description" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once a file is selected, we call a &lt;a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#forms" rel="noopener noreferrer"&gt;Server Action&lt;/a&gt; with the &lt;code&gt;FormData,&lt;/code&gt; which triggers a &lt;code&gt;”contacts.uploaded”&lt;/code&gt; event to Inngest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/inngest/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contacts.uploaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;contactFileContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The front end does not expect the import to be instantaneous, and also, because the call to OpenAI's o1-preview may take multiple minutes, the actual processing and generation of the contact import workflow are performed in the background by an Inngest Function: &lt;code&gt;generateImportWorkflow()&lt;/code&gt;. Let's look at its implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking OpenAI o1 to generate an import workflow
&lt;/h2&gt;

&lt;p&gt;Our first Inngest Function, &lt;code&gt;generateImportWorkflow(),&lt;/code&gt; leverages Vercel Edge Function's streaming capabilities to call OpenAI's &lt;code&gt;o1-preview&lt;/code&gt; model and generate a tailored import workflow for our contact list without duration limitation.&lt;/p&gt;

&lt;p&gt;Let's take a closer look at our &lt;code&gt;o1-preview&lt;/code&gt; prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactsFileContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
Considering the following available actions:
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openaiCall&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`- &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, name: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;

and, given the below CSV file (between &lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;), recommend the best steps by balancing existing actions and using custom openai calls (do not use deprecated models) to successfully:
    - parse the contact information and remove any typos
    - enrich the contacts data
    - label them a decider and rank them as a good Sales target to sale software to based on the provided property
    - rework the CSV file to match the provided column to the following: Name, Position, Company, Email and the ranking and decider columns
    - save the contacts to the database

Return a JSON array made of steps to execute. When a step is a model call, provide the "model" for model name and "prompt" for the prompt with "{data}" as a placeholder for the provided data; When the step is an action, provide the action name.

&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contactsFileContent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above prompts demonstrate the power of reasoning models like &lt;code&gt;o1-preview&lt;/code&gt;. From high-level goals, the model takes a few minutes to find the best chain of predefined actions and additional model calls to perform to successfully parse, transform, rank, and save the contacts.&lt;/p&gt;

&lt;p&gt;We are using the stellar &lt;a href="https://sdk.vercel.ai/docs/reference/ai-sdk-core/generate-text" rel="noopener noreferrer"&gt;Vercel AI SDK&lt;/a&gt; to call &lt;code&gt;o1-preview&lt;/code&gt;. Here is an example of the actions generated by the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"convert"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-3.5-turbo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Parse the following contact information from JSON, correct any typos, and ensure all fields are properly formatted."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enrich"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-3.5-turbo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Label each contact as a decider and rank them as a good sales target for software based on their company, role, and industry."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-3.5-turbo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reformat the following JSON data to match the columns: Name, Position, Company, Email."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"save"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actions generated will vary depending on the provided CSV file.&lt;br&gt;
The code then transforms these actions into a format using &lt;a href="https://www.inngest.com/docs/reference/workflow-kit/workflow-instance" rel="noopener noreferrer"&gt;Inngest's Workflow Kit&lt;/a&gt; which can easily handle o1's multi-step plans and execute them. The transformed plan is then send to the importContacts Inngest Function to execute the plan.  &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;strong&gt;Note: &lt;code&gt;o1-preview&lt;/code&gt; vs. &lt;code&gt;o1-mini&lt;/code&gt; accuracy&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;o1-mini&lt;/code&gt; is a preferred choice when developing, offering faster iterations and lower costs. While developing this demo, we realized that &lt;code&gt;o1-mini&lt;/code&gt; struggled to output a deterministic and simple JSON structure for model call actions. Switching back to &lt;code&gt;o1-preview&lt;/code&gt; solved this problem.&lt;br&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;o1-preview&lt;/code&gt; call and the processing of its results is wrapped as an Inngest Function, enabling it to run without duration limitation in the background and matching &lt;code&gt;o1-preview&lt;/code&gt;'s &lt;a href="https://x.com/OpenAIDevs/status/1841176573527077235" rel="noopener noreferrer"&gt;API Rate limits&lt;/a&gt; by leveraging &lt;a href="https://www.inngest.com/docs/guides/throttling" rel="noopener noreferrer"&gt;throttling&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// imports ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generate-import-workflow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contacts.uploaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generatedStepsResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai-o1-generate-steps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="c1"&gt;// ...&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Access the full source code &lt;a href="https://github.com/inngest/vercel-ai-open-o1-crm-agent/blob/main/lib/inngest/functions/generateImportWorkflow.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Executing the dynamic workflow with Inngest's Workflow Kit
&lt;/h2&gt;

&lt;p&gt;Our &lt;code&gt;importContact()&lt;/code&gt; Inngest Function leverages the &lt;a href="https://www.inngest.com/blog/introducing-workflow-kit" rel="noopener noreferrer"&gt;Inngest Workflow Kit&lt;/a&gt; to execute a dynamic list of steps passed in the &lt;code&gt;”contacts.process”&lt;/code&gt; event's body.&lt;/p&gt;

&lt;p&gt;Our Inngest workflow is composed of 4 predefined actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;openaiCall&lt;/code&gt;: Make a dynamic call to OpenAI API
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;convert&lt;/code&gt;: convert CSV data to JSON items&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enrich&lt;/code&gt;: Enrich contact information
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;save&lt;/code&gt;: Save contact information to the database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our &lt;code&gt;o1-preview&lt;/code&gt; model leverages the &lt;code&gt;openaiCall&lt;/code&gt; action to dynamically add actions, helping to format and structure the provided contact data. It takes two parameters: &lt;code&gt;prompt&lt;/code&gt;, and &lt;code&gt;model&lt;/code&gt; which the model configures according to the task to achieve.&lt;/p&gt;

&lt;p&gt;Each workflow's action provides a &lt;code&gt;handler()&lt;/code&gt; function that benefits from Inngest's features, such as &lt;a href="https://www.inngest.com/blog/durable-functions-a-visual-typescript-primer" rel="noopener noreferrer"&gt;automatic retries&lt;/a&gt; and throttling. These features improve the reliability of our agentic workflow and help manage its unpredictability.&lt;/p&gt;

&lt;p&gt;An Inngest Workflow is then passed to our &lt;code&gt;importContacts()&lt;/code&gt; Inngest Function for execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EngineAction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inngest/workflow-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EngineAction&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="c1"&gt;// other actions ...&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;save&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Save contacts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;save contact information to the database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;save-contacts-to-database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contacts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`INSERT INTO contacts (Name,Position,Company,Email,Decider,Ranking) VALUES &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contacts&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`('&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Decider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Ranking&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowEngine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workflowInstance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;import-contacts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contact.process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// When `run` is called, the loader function is called with access to the event&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;workflowEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Access the full source code &lt;a href="https://github.com/inngest/vercel-ai-open-o1-crm-agent/blob/main/lib/inngest/functions/importContacts.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How our Agent performs
&lt;/h2&gt;

&lt;p&gt;Our demo CRM application now features a smooth AI-powered contact import onboarding experience powered by a semi-autonomous agentic workflow as a complete part of the Next.js application ✨&lt;br&gt;&lt;br&gt;
Uploading the below CSV file sample generates a custom import workflow and runs smoothly in the background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First Name,Last Name,Email,Seniority,Role,Company,Industry
Amanda,Brown,amanda.brown@example.com,VP,Data Scbentist,AutoCorp,Real Estate
Sarah,Johnson,sarah.johnson@example.com,Junior,CEO,BioGen,Education
Michael,Jackson,michael.jackson@example.com,Junior,CqO,AutoCorp,Healthcare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Notice the typos in position, roles, and columns that do not match our contact properties.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once completed, the clean, structured, and enriched contact records are saved to the database:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F8hv4f2ob29i2ah1cvjfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F8hv4f2ob29i2ah1cvjfi.png" alt="Image description" width="800" height="620"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Vercel Postgres database with the contact information loaded from the agentic workflow. &lt;strong&gt;Our Agent event fixed the typos in contact properties like "Scbentist"&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When deployed on Vercel, the workflow leverages Edge Function's streaming capabilities to reliably call &lt;code&gt;o1-preview&lt;/code&gt; without duration considerations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making our agent even smarter
&lt;/h3&gt;

&lt;p&gt;This contacts import on autopilot is a first draft that can be used as a boilerplate to iterate and improve &lt;code&gt;o1&lt;/code&gt;'s results.&lt;br&gt;&lt;br&gt;
A non-exhaustive list of interesting additional capabilities could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leveraging the OpenAI Batching API to reduce cost on large CSV files
&lt;/li&gt;
&lt;li&gt;Have the model learn from previous runs to fine-tune its lead ranking mechanism
&lt;/li&gt;
&lt;li&gt;Iterate and improve &lt;code&gt;o1&lt;/code&gt;'s prompt by following &lt;a href="https://platform.openai.com/docs/guides/reasoning/advice-on-prompting" rel="noopener noreferrer"&gt;OpenAI's best practices&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try the demo now
&lt;/h2&gt;

&lt;p&gt;The combination of Vercel AI SDK and Inngest makes it super easy to add Agentic workflows to provide more accurate (e.g., support chat) or achieve more complex tasks (e.g., data analysis, agent-based features).&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Try this demo now&lt;/strong&gt; by running it locally or deploying it on Vercel: &lt;a href="https://github.com/inngest/vercel-ai-o1-preview-crm-agent" rel="noopener noreferrer"&gt;https://github.com/inngest/vercel-ai-open-o1-crm-agent&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>openai</category>
      <category>javascript</category>
      <category>learning</category>
    </item>
    <item>
      <title>What is waitUntil (Vercel, Cloudflare) and when should I use it?</title>
      <dc:creator>Dan Farrelly</dc:creator>
      <pubDate>Wed, 22 May 2024 17:06:55 +0000</pubDate>
      <link>https://dev.to/inngest/what-is-waituntil-vercel-cloudflare-and-when-should-i-use-it-kol</link>
      <guid>https://dev.to/inngest/what-is-waituntil-vercel-cloudflare-and-when-should-i-use-it-kol</guid>
      <description>&lt;p&gt;Recently, &lt;a href="https://vercel.com/changelog/waituntil-is-now-available-for-vercel-functions"&gt;Vercel announced&lt;/a&gt; that their &lt;code&gt;waitUntil&lt;/code&gt; utility method is now available for all serverless functions. Now, this useful feature is available for &lt;em&gt;both&lt;/em&gt; Vercel's Node.js and edge functions. This utility is not unique to Vercel -- &lt;a href="https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil"&gt;Cloudflare Workers&lt;/a&gt; also offers a similar one.&lt;/p&gt;

&lt;p&gt;This is a very useful tool so let's dive into what &lt;code&gt;waitUntil&lt;/code&gt; is and what it does, when to use it, and when it's better not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does &lt;code&gt;waitUntil&lt;/code&gt; do
&lt;/h2&gt;

&lt;p&gt;To understand what &lt;code&gt;waitUntil&lt;/code&gt; does and why it exists, let's talk about how serverless functions work.&lt;/p&gt;

&lt;p&gt;Serverless functions are designed to be &lt;a href="https://www.redhat.com/en/topics/cloud-native-apps/stateful-vs-stateless"&gt;stateless&lt;/a&gt; and short-lived. Unlike traditional servers, serverless functions are not kept alive between requests. Instead, they are spun up on-demand to handle incoming requests and then shut down once the response is sent.&lt;/p&gt;

&lt;p&gt;With JavaScript serverless functions, you have to &lt;code&gt;await&lt;/code&gt; all asynchronous operations to ensure that the function doesn't return before the asynchronous operation is complete. If you don't &lt;code&gt;await&lt;/code&gt; an asynchronous operation, the serverless function might be shut down before the operation is complete. If this happens, the operation might be canceled or fail which results in unpredictable behavior. This is why it's important to &lt;code&gt;await&lt;/code&gt; all asynchronous operations in serverless functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ BAD: If sendMetrics is an async function (a Promise), there is no guarantee it will succeed&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;sendMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ GOOD: Awaiting will ensure it completes before the function returns&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downside of using &lt;code&gt;await&lt;/code&gt; is that it can slow down the response time of your serverless functions. You ideally want to &lt;a href="https://www.epicweb.dev/talks/improve-performance-and-reliability-of-your-api-with-events-and-background-functions"&gt;keep the critical path of your request minimal&lt;/a&gt; so you return the fastest response possible for your users. This is where &lt;code&gt;waitUntil&lt;/code&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;waitUntil&lt;/code&gt; allows you to ensure that an async function (a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"&gt;&lt;code&gt;Promise&lt;/code&gt;&lt;/a&gt;) completes before the serverless function is shut down. As the example code above doesn't require any data returned from &lt;code&gt;sendMetrics&lt;/code&gt;, we can use &lt;code&gt;waitUntil&lt;/code&gt; to ensure that the async function completes in the background without blocking the response from returning to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD: The sendMetrics function will execute in the background,&lt;/span&gt;
&lt;span class="c1"&gt;// but not block the response from returning to the user&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sendMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, is &lt;code&gt;waitUntil&lt;/code&gt; a solution to all possible problems? Well, it is a very useful tool and it ensures completion. However, it does not ensure &lt;em&gt;success&lt;/em&gt;. For example, if the promise passed to &lt;code&gt;waitUntil&lt;/code&gt; fails, the serverless function will still complete and return a response. So when should we use it?&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use &lt;code&gt;waitUntil&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;waitUntil&lt;/code&gt; is useful when you need to perform some asynchronous work that is not critical to the response of the serverless function. This is useful for things like sending metrics, logging, cache control, or other things that should not block the user's response. The user should not pay the penalty for these operations, so they can be done in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  When &lt;em&gt;not&lt;/em&gt; to use &lt;code&gt;waitUntil&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;As mentioned above, &lt;code&gt;waitUntil&lt;/code&gt; does not ensure &lt;em&gt;success&lt;/em&gt;. If the promise passed to &lt;code&gt;waitUntil&lt;/code&gt; fails, the serverless function will still complete and return a response. This is important to keep in mind when deciding when to use &lt;code&gt;waitUntil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In other words, &lt;code&gt;waitUntil&lt;/code&gt; is not the tool for the job if the asynchronous operation affects critical business logic in your application (which is precisely what makes it great for logging and metrics!). If the operation fails, it's not the end of the world. But if the operation is critical to the response of the serverless function, &lt;code&gt;await&lt;/code&gt; is the way to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I run background jobs with &lt;code&gt;waitUntil&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Short answer: No.&lt;/p&gt;

&lt;p&gt;Long answer: &lt;code&gt;waitUntil&lt;/code&gt; is not designed for running background jobs. There are no retries or tools for handling failures. Also, &lt;code&gt;waitUntil&lt;/code&gt; will only run for as long as your function's timeout is set for, meaning if something takes longer than your timeout, it will be canceled. For running background jobs, you should use a system that handles queueing, retries, and logs failures.&lt;/p&gt;

&lt;p&gt;Inngest is designed to handle background jobs reliably. It automatically handles retries, logs, and failures. If you need to run background jobs, you should consider using Inngest instead of &lt;code&gt;waitUntil&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should I use &lt;code&gt;waitUntil&lt;/code&gt; with Inngest?
&lt;/h2&gt;

&lt;p&gt;Inngest functions are truly async, background jobs. To trigger Inngest functions, you use &lt;code&gt;inngest.send()&lt;/code&gt; which sends the data to the Inngest &lt;em&gt;Event API&lt;/em&gt;. Inngest then triggers the function and handles retries on it's end. The &lt;code&gt;inngest.send()&lt;/code&gt; function returns a &lt;code&gt;Promise&lt;/code&gt; so it is possible to use with &lt;code&gt;waitUntil&lt;/code&gt;. Should you use it though?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/inngest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inngest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typically, Inngest functions contain core business logic, so you need to ensure that the functions are triggered successfully. The key question is the reliability of &lt;code&gt;waitUntil&lt;/code&gt; and the function it calls. Let's break down some things to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Inngest Event API has very high uptime and is designed to be reliable.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;inngest.send()&lt;/code&gt; function is very simple - mostly just an &lt;code&gt;POST&lt;/code&gt; request to the Event API.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;waitUntil&lt;/code&gt; utility from Vercel and Cloudflare is designed to be reliable.&lt;/li&gt;
&lt;li&gt;Networking issues or blips can always cause issues with outbound requests from any platform.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;waitUntil&lt;/code&gt; will only last as long as the timeout for the function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given these points, it is safe to use &lt;code&gt;waitUntil&lt;/code&gt; with &lt;code&gt;inngest.send()&lt;/code&gt; with the caveat that if there is a networking issue, the event may not reach Inngest. While this is a rare occurrence, it is a risk to be aware of and is a trade off that you need to decide for yourself.&lt;/p&gt;

&lt;p&gt;If you don't want to take this risk, you can use &lt;code&gt;inngest.send()&lt;/code&gt; without &lt;code&gt;waitUntil&lt;/code&gt;, but it will be blocking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/inngest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want &lt;strong&gt;the best of both worlds&lt;/strong&gt;, you might determine a recovery strategy for when the event doesn't reach Inngest. This could be as simple as logging the failure and retrying the event later. Below is an incomplete example of how you might handle a failure and do something with the event. Not included is any logic you might need for retrying the event later using whatever logging backend or similar that you use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/inngest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;inngest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Log the event somewhere where you could read and re-send it later&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;waitUntil&lt;/code&gt; is a highly useful tool to have in your serverless toolkit. It can help keep async code out of the critical path of the request to return the response to the user as fast as possible. It is very important to understand when to use it and when not to use it. It's also important to understand the risks of using it with other services.&lt;/p&gt;

&lt;p&gt;Ideally, in the future, these services might offer some sort of out-of-the-box logging and retry mechanism for &lt;code&gt;waitUntil&lt;/code&gt;. Until then, you should be aware of the risks and decide if it's right for your use case.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>react</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Debounce messages in queueing systems: How to do it with Postgres</title>
      <dc:creator>Dan Farrelly</dc:creator>
      <pubDate>Fri, 05 Apr 2024 16:23:09 +0000</pubDate>
      <link>https://dev.to/inngest/debounce-messages-in-queueing-systems-how-to-do-it-with-postgres-4jmj</link>
      <guid>https://dev.to/inngest/debounce-messages-in-queueing-systems-how-to-do-it-with-postgres-4jmj</guid>
      <description>&lt;p&gt;When developers talk about debouncing, it's most often discussed as a front-end development technique to handle issues in user interfaces. It's also quite a useful technique to apply to backend message queuing systems. In this post we'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what debouncing is,&lt;/li&gt;
&lt;li&gt;when to use debouncing with queues,&lt;/li&gt;
&lt;li&gt;how to implement this in a simple Posgtres queue,&lt;/li&gt;
&lt;li&gt;and how you can use this feature with Inngest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is debouncing
&lt;/h2&gt;

&lt;p&gt;Debouncing is used to avoid unnecessary executions. At its core, it is a technique to ensure a particular task or operation is executed only once after a specified delay since its last invocation, even if triggered multiple times within that delay.&lt;/p&gt;

&lt;p&gt;A classic front-end example involves an auto-complete input where a user's actions, like typing, could trigger duplicate work by fetching search results. The goal is to send a single action, such as an HTTP request, only after the user has finished typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use debouncing in queuing systems
&lt;/h2&gt;

&lt;p&gt;Debouncing within queueing systems is most important when you're aiming to improve efficiency of execution. It might be a user that triggers a job that's costly to run or some event that triggers a larger job to execute that is wasteful to have to re-run. Here are couple of specific situations where you may want to add debouncing into your queue and worker system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost efficiency with AI / LLMs.&lt;/strong&gt;
Leveraging any sort of AI is expensive. Whether you're using OpenAI's APIs or running your own models on GPU-optimized instances, leveraging AI can be costly. Depending on what is triggering your AI code to run (for example, a user saves data in a form) it may be triggered twice within a short time window. Debouncing this trigger can help ensure there is no wasted re-work with AI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data synchronization and integrity.&lt;/strong&gt;
In systems involving data synchronization or batch processing, it's often essential to ensure that operations are performed only when all relevant changes have been completed. Debounce can facilitate this by delaying execution until a predefined idle period has passed, ensuring data integrity and consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User interface interactions and auto-save.&lt;/strong&gt; Similar to the example mentioned above,  background jobs are often triggered by user actions that save data. If a user saves the same data in quick succession, or perhaps the front-end implements auto-save, it is likely that multiple background jobs will be triggered. Adding a debounce delay to your background job can make your system more efficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicating chatty webhook events.&lt;/strong&gt; While webhook events are great to integrate with third-party systems, some webhook providers, for example Intercom, may trigger multiple events in short succession for the same or similar actions that you only need to react to once. Also, often when building webhook integrations, you'd design for eventual consistency, so debounce delays are acceptable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While debouncing is traditionally associated with frontend development and user interface interactions, its principles and techniques can also be applied effectively in backend software development to optimize resource utilization, mitigate performance bottlenecks, and enhance the scalability and reliability of distributed systems and event-driven architectures.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to implement debouncing in a message queue
&lt;/h2&gt;

&lt;p&gt;Depending on what backing queue you use, implementing debounce can be simple or difficult. At the basic level, your system needs to be able to schedule a given message at a timestamp in the future (the delay) and when the same message is sent, the currently scheduled message's timestamp is “pushed” back by the delay amount again. Your queue then must only pick up messages that are ready (the current timestamp).&lt;/p&gt;

&lt;p&gt;Here we'll cover a basic implementation of a Postgres queue and look at how to add debounce. There are more advanced techniques for queuing in Postgres, but we will not cover that here.&lt;/p&gt;

&lt;p&gt;First, we'll create a simple &lt;code&gt;debounce_job_queue&lt;/code&gt; table in our database. It will have a job &lt;code&gt;id&lt;/code&gt; , a &lt;code&gt;job_type&lt;/code&gt; , a current &lt;code&gt;status&lt;/code&gt; for the job, a &lt;code&gt;payload&lt;/code&gt; for relevant job data, and a &lt;code&gt;scheduled_at&lt;/code&gt; timestamp. Lastly, we'll add a &lt;code&gt;debounce_key&lt;/code&gt; which will be what we'll use to de-duplicate and delay work.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;debounce_job_queue&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;job_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;scheduled_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;debounce_key&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;As we want to de-duplicate jobs waiting the queue, we'll add a unique index to ensure that there is only one matching job that is &lt;code&gt;'QUEUED'&lt;/code&gt; at a given time.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;debounce_key_status&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;debounce_job_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debounce_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'QUEUED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Creating a new job in this queue is a relatively straightforward insert, but we add a few things that help us implement debounce:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a deterministic &lt;code&gt;debounce_key&lt;/code&gt; using a hash of the job's type and payload JSON data. Alternatively, this could be any unique piece of data like a user's ID.&lt;/li&gt;
&lt;li&gt;Allow the calling function to specify a delay when setting the &lt;code&gt;scheduled_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Handle conflicts with existing queued items with an &lt;code&gt;UPDATE&lt;/code&gt; to the existing item's &lt;code&gt;scheduled_at&lt;/code&gt; with the specific delay. &lt;strong&gt;This is where the debouncing happens.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll use the &lt;code&gt;@vercel/postgres&lt;/code&gt; library's simple &lt;code&gt;sql&lt;/code&gt; here, but your favorite library will do just fine.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vercel/postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debounceKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobType&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`
        INSERT INTO debounce_job_queue (
            job_type, status, payload, scheduled_at, debounce_key
        )
        VALUES (
            '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jobType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', 'QUEUED', '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;',
            now() + INTERVAL '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;debounceKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
        )
        -- If the item is already QUEUED, update the scheduled at w/ the delay
        ON CONFLICT (debounce_key, status) WHERE status = 'QUEUED'
        DO UPDATE SET scheduled_at = now() + INTERVAL '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds';
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The code above may appear complex at first, but the magic happens in the &lt;code&gt;ON CONFLICT&lt;/code&gt; statement. Our unique index ensures that two identical jobs cannot be &lt;code&gt;'QUEUED'&lt;/code&gt; at the same time. When the query attempts to insert a job and there is an identical job that is &lt;code&gt;QUEUED&lt;/code&gt;, the conflict statement is executed and the existing job's scheduled at is updated with our delay to push it back.&lt;/p&gt;

&lt;p&gt;To query for available jobs, we'll also now use our &lt;code&gt;scheduled_at&lt;/code&gt; timestamp to ensure that our function only fetches jobs that are ready to be processed. When jobs are debounced and the &lt;code&gt;scheduled_at&lt;/code&gt; is pushed into the future, that prevents our query from fetching that job.&lt;/p&gt;

&lt;p&gt;In our query, we'll also update the &lt;code&gt;status&lt;/code&gt; to &lt;code&gt;PROCESSING&lt;/code&gt; to mark that this job should not be picked up by any other worker and that it shouldn't be debounced again as it's being executed. In our code, we'll omit the worker polling logic and focus on the query. We'll use &lt;code&gt;SKIP LOCKED&lt;/code&gt; here to prevent only query for rows that are locked due to being updated elsewhere.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getQueueItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`
    UPDATE debounce_job_queue
    SET status = 'PROCESSING'
    WHERE id = (
      SELECT id FROM debounce_job_queue
      WHERE status = 'QUEUED' and scheduled_at &amp;lt;= now() + INTERVAL '2 minute'
      ORDER BY scheduled_at ASC
      FOR UPDATE SKIP LOCKED
      LIMIT 1
    )
    RETURNING id, payload;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That's it! In your own worker code you'll want to also &lt;code&gt;DELETE&lt;/code&gt; this item from the queue table when it's done processing (or similar). If you are implementing a debounce queue in Postgres, you may want to additionally consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding other indexes to your table for performance.&lt;/li&gt;
&lt;li&gt;Adding a job expiration timestamp and clearing of expired jobs.&lt;/li&gt;
&lt;li&gt;Adding a column to track the number of attempts to process a given item and handling a maximum number of retries.&lt;/li&gt;
&lt;li&gt;Implementing a dead letter queue.&lt;/li&gt;
&lt;li&gt;Learning more about using &lt;code&gt;SKIP LOCKED&lt;/code&gt; .&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we'll compare this to implementing debounce with Inngest.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to debounce with Inngest functions
&lt;/h2&gt;

&lt;p&gt;Inngest's architecture combines an event stream, queues, and durable execution into a single reliability layer for your application. With Inngest's internal queueing system, you can easily debounce jobs.&lt;/p&gt;

&lt;p&gt;The main differences to the above example is that with Inngest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You send events instead of messages.&lt;/li&gt;
&lt;li&gt;You define functions which each has their own queue(s) created automatically for you.&lt;/li&gt;
&lt;li&gt;Inngest calls your function for you, so you don't need to implement polling or similar in a worker.&lt;/li&gt;
&lt;li&gt;You can define your debounce key using any piece of data within your event's payload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.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%2F4mjf6fw0gixpq050h73s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F4mjf6fw0gixpq050h73s.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configuring debouncing for an Inngest function is a couple of lines of code. In our example below, we'll create a function that generates suggested titles for a blog post every time a user saves a draft. We'll add a debounce &lt;code&gt;period&lt;/code&gt; of one minute and we'll set a debounce &lt;code&gt;key&lt;/code&gt; to only debounce based off the given &lt;code&gt;blog_post_id&lt;/code&gt; . Setting the key will make sure that each unique blog post will be separately debounced.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateIdeasFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inngest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generate-ideas-via-open-ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;event.data.blog_post_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog/draft.saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* call OpenAI */&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;For more information on this, check out &lt;a href="https://www.inngest.com/docs/reference/functions/debounce" rel="noopener noreferrer"&gt;our guide on debouncing&lt;/a&gt; or check out &lt;a href="https://www.inngest.com/docs/quick-start" rel="noopener noreferrer"&gt;our quick start tutorial&lt;/a&gt; to learn the basics of Inngest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Debouncing is a powerful and highly useful technique in many queueing use cases. This is why we built it right into Inngest so any developer can use it with any function with ease. &lt;a href="https://www.inngest.com/discord" rel="noopener noreferrer"&gt;We'd love to hear&lt;/a&gt; what other use cases where you use debouncing and any other features that you have added!&lt;/p&gt;

&lt;p&gt;If you found this blog post interesting, check out another one on &lt;a href="https://www.inngest.com/blog/building-the-inngest-queue-pt-i-fairness-multi-tenancy" rel="noopener noreferrer"&gt;how we've built our multi-tenant queuing system&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>cloud</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
