Stack logo
Sync up on the latest from Convex.
Hyo Jang's avatar
Hyo Jang
5 days ago

How to Schedule AI Content Creation Using Convex

This article is part of our 4-part series that covers the steps to build a fully functional app using Convex and Expo.

  1. User Authentication with Clerk and Convex
  2. Real-Time User Profiles with Convex and Clerk
  3. Implementing Real-Time AI Chat with Convex
  4. Automating Content with Convex Cron and ChatGPT (You are here)

 

Building on our previous implementation of ChatGPT-powered conversations, we'll now explore how to automate content generation using Convex's scheduling capabilities.

Now, we’re taking it a step further by automatically generating new knowledge from ChatGPT every minute for testing purposes. In this post, we’ll dive into Convex’s Cron functionality and explore how to effectively integrate it into an Expo app.

Overview

What will be covered?

This post demonstrates how to use Convex's Cron functionality to automate backend tasks. We'll build a practical example that automatically generates and stores Q&A content about Convex using ChatGPT.

Even if you’re not familiar with Convex, you can gain insights into its features and capabilities simply by following along. This automated scenario can serve as a practical reference for developers seeking inspiration for their own app projects.

Convex simplifies the implementation of Cron jobs, making it easier to automate processes in your backend.

What you'll learn

By the end of this section, you’ll have learned:

  1. Automating Q&A Generation

Discover how to use ChatGPT to automatically generate questions about a specific topic (e.g., Convex) and retrieve concise, useful answers.

  1. Using Convex Cron

Learn how to automate tasks at regular intervals using Cron jobs and integrate them seamlessly with Convex.

  1. Understanding Convex Actions

Explore how Convex Actions can be used to handle external API calls and asynchronous operations in your backend logic.

  1. Implementing Internal Functions

Learn how to write secure, server-only logic with Convex’s Internal Functions to ensure data integrity and privacy.

  1. Expanding Development Ideas

Apply these concepts to develop automated information systems or periodic data updates for other use cases, fueling your creativity in app development.

What is Convex Cron?

Convex Cron is a powerful scheduling feature that enables you to automatically execute tasks at specific intervals in your Convex backend. It simplifies repetitive backend operations and ensures reliable workflow automation.

Key Features

  • Serverless Environment. Schedule tasks without managing servers, ensuring a stable and scalable backend.

  • Precision Timing. Define task intervals using standard cron expressions or simple time-based configurations.

  • Seamless Integration. Easily integrate Cron jobs with Convex databases and APIs.

Implementing a Cron Job that Runs Every Minute

Here’s a step-by-step guide to implementing a minute-by-minute Cron job using Convex.

Step 1: Write an Internal Mutation

Since Cron jobs can’t directly call standard mutations, we need to create an Internal Mutation to handle data insertion into the messages table, then set up our scheduled task.

The saveMessage function we implemented in the last article cannot be directly used in a cron job. For cron jobs, you need to implement and execute the function using Internal Functions, as explained in the Convex documentation: Internal Functions.

Here’s how you define an Internal Mutation to add data to the messages table:

1export const insertAiMessage = internalMutation({
2  args: v.object({
3    message: v.string(),
4    reply: v.string(),
5    author: v.id('users'),
6  }),
7  handler: async (ctx, {message, reply, author}) => {
8    await ctx.db.insert('messages', {
9      author,
10      message,
11      reply,
12    });
13  },
14});
15

Step 2: Creating a Cron Job

Now, let’s set up a Cron job that runs every minute.

Create a convex/crons.ts file with the following implementation:

Here’s how the Cron job implementation looks:

1import {cronJobs} from 'convex/server';
2import {internal} from './_generated/api';
3import {Id} from './_generated/dataModel';
4
5const crons = cronJobs();
6
7crons.interval(
8  'Add AI message every minute',
9  {minutes: 1},
10  internal.messages.insertAiMessage,
11  {
12    author: 'j97d6x6smxpb8p2rhk96g13mpd71znb1' as Id<'users'>,
13    message: 'Hello',
14    reply: 'Hello, how can I help you?',
15  },
16);
17
18export default crons;
19

Key Configuration

  • 🤔Interval Setting: {minutes: 1} tells Convex to run this job every minute. You can adjust this interval based on your needs.
  • Function Reference: internal.messages.insertAiMessage specifies which Internal Mutation to call.
  • Author ID: The author field requires a valid user ID. Using a hardcoded ID like 'j97d6x6smxpb8p2rhk96g13mpd71znb1' is not recommended for production, but work for our testing purposes.

Once you save the Cron function and start the server (npx convex dev), you should see the Cron job running as expected by monitoring the job execution in your app. You should see it populate the database with new data every minute.

Enhancing with ChatGPT API

Now that the Cron job is working, let’s integrate the ChatGPT API to generate random questions and answers automatically. Here's how to extend your existing insertAiMessage function:

1export const insertAiMessage = internalMutation({
2  args: v.object({
3    message: v.string(),
4    reply: v.string(),
5    author: v.id('users'),
6  }),
7  handler: async (ctx, {message, reply, author}) => {
8    const apiKey = process.env.OPENAI_API_KEY;
9
10    if (!apiKey) {
11      throw new Error('OpenAI API key is not set in environment variables.');
12    }
13
14    const systemPrompt =
15      'You are an assistant that generates random questions about the Convex framework and answers them concisely. Do not use prefixes like "Question:" or "Answer:" in your response.';
16
17    const response = await fetch('https://api.openai.com/v1/chat/completions', {
18      method: 'POST',
19      headers: {
20        'Content-Type': 'application/json',
21        Authorization: `Bearer ${apiKey}`,
22      },
23      body: JSON.stringify({
24        model: 'gpt-4o-mini',
25        messages: [
26          {role: 'system', content: systemPrompt},
27          {
28            role: 'user',
29            content: 'Generate a random question about Convex and answer it.',
30          },
31        ],
32      }),
33    });
34
35    console.log('response', response);
36    
37    ...
38  },
39});
40

Encountering Issues with the Fetch Function

After implementing the code, I eagerly waited for the updated AI-generated message. Unfortunately, nothing happened 🤨. Curious about what went wrong, I checked the Convex dashboard logs and found the following errors:

The logs revealed that the fetch function cannot be used directly within queries or mutations. Instead, Convex recommends implementing an Action for such cases, which we'll cover in the next section.

Step 3: Implementing a Convex Action

To resolve this issue, I created an Action to handle the fetch call. Follow these steps to do the same:

  1. Create the Action File

In your project, create a file named convex/actions.tsto implement the Action function.

  1. Set Up Environment Variables

Ensure your OpenAI API key is securely stored as an environment variable in Convex. Use the following command:

1npx convex env set OPENAI_API_KEY your-open-ai-key
2

Refer to the Convex documentation for more details on managing environment variables.

Writing the Action

Here's how to implement the Action step by step:

  1. Inject the OpenAI API Key

    1const apiKey = process.env.OPENAI_API_KEY;
    2
    3if (!apiKey) {
    4	throw new Error('OpenAI API key is not set in environment variables.');
    5}
    6
    7const systemPrompt =
    8	'You are an assistant that generates random questions about the Convex framework and answers them concisely. Do not use prefixes like "Question:" or "Answer:" in your response.';
    9
  2. Define the System Prompt

    Create a prompt instructing ChatGPT to generate concise and useful responses:

    1const systemPrompt = 
    2  'You are an assistant that generates random questions about the Convex framework and answers them concisely. Do not use prefixes like "Question:" or "Answer:" in your response.';
    3
  3. Call the OpenAI API

    Use the fetch function to interact with OpenAI’s API:

    1const response = await fetch('https://api.openai.com/v1/chat/completions', {
    2  method: 'POST',
    3  headers: {
    4    'Content-Type': 'application/json',
    5    Authorization: `Bearer ${apiKey}`,
    6  },
    7  body: JSON.stringify({
    8    model: 'gpt-4o-mini',
    9    messages: [
    10      { role: 'system', content: systemPrompt },
    11      { role: 'user', content: 'Generate a random question about Convex and answer it.' },
    12    ],
    13  }),
    14});
    15
    16if (!response.ok) {
    17  throw new Error(`HTTP error! status: ${response.status}`);
    18}
    19
  4. Store the Result in the Database

    Parse the response and pass the result to the insertAiMessage mutation:

    1const data = await response.json();
    2const assistantMessage = data.choices?.[0]?.message?.content;
    3
    4if (!assistantMessage) {
    5  throw new Error('Failed to retrieve a valid response from OpenAI.');
    6}
    7
    8const [question, ...answerParts] = assistantMessage.split('\n');
    9
    10await ctx.runMutation(internal.messages.insertAiMessage, {
    11  author,
    12  message: question,
    13  reply: answerParts.join('\n').trim(),
    14});
    15

Complete Action Implementation

Here's the full implementation for action.ts:

1import {internalAction} from './_generated/server';
2import {internal} from './_generated/api';
3import {v} from 'convex/values';
4
5export const fetchAndInsertAiMessage = internalAction({
6  args: v.object({
7    author: v.id('users'),
8  }),
9  handler: async (ctx, {author}) => {
10    const apiKey = process.env.OPENAI_API_KEY;
11
12    if (!apiKey) {
13      throw new Error('OpenAI API key is not set in environment variables.');
14    }
15
16    const systemPrompt =
17      'You are an assistant that generates random questions about the Convex framework and answers them concisely. Do not use prefixes like "Question:" or "Answer:" in your response.';
18
19    const response = await fetch('https://api.openai.com/v1/chat/completions', {
20      method: 'POST',
21      headers: {
22        'Content-Type': 'application/json',
23        Authorization: `Bearer ${apiKey}`,
24      },
25      body: JSON.stringify({
26        model: 'gpt-4o-mini',
27        messages: [
28          {role: 'system', content: systemPrompt},
29          {
30            role: 'user',
31            content: 'Generate a random question about Convex and answer it.',
32          },
33        ],
34      }),
35    });
36
37    if (!response.ok) {
38      throw new Error(`HTTP error! status: ${response.status}`);
39    }
40
41    const data = await response.json();
42    const assistantMessage = data.choices?.[0]?.message?.content;
43
44    if (!assistantMessage) {
45      throw new Error('Failed to retrieve a valid response from OpenAI.');
46    }
47
48    const [question, ...answerParts] = assistantMessage.split('\n');
49
50    await ctx.runMutation(internal.messages.insertAiMessage, {
51      author,
52      message: question,
53      reply: answerParts.join('\n').trim(),
54    });
55
56    return {question, answer: answerParts};
57  },
58});
59

Step 4: Updating the Cron Job

Now let's modify our Cron job to use the Action we just created. Update your crons.ts file:

1import {cronJobs} from 'convex/server';
2import {internal} from './_generated/api';
3import {Id} from './_generated/dataModel';
4
5const crons = cronJobs();
6
7crons.interval(
8  'Add AI message every minute',
9  {minutes: 1},
10+  internal.actions.fetchAndInsertAiMessage,
11  {author: 'j97d6x6smxpb8p2rhk96g13mpd71znb1' as Id<'users'>},
12);
13
14export default crons;
15

Final Output

After implementing the Action and updating the Cron job, your application will automatically generate and store Q&A content about Convex every minute:

This implementation demonstrates several key Convex features:

  • Cron jobs for automated task scheduling
  • Actions for external API integration
  • Real-time database updates that automatically reflect in your UI
  • Type-safe backend operations with TypeScript

It also opens up exciting possibilities for AI-driven applications. For example, apps that deliver personalized educational content to children or provide real-time information updates. Convex makes it incredibly simple to build these features by offering seamless database integration, scheduling, and a clean dashboard for monitoring.

Conclusion

This tutorial has walked you through building a practical automated system with Convex. Starting from basic database operations, we progressed to creating a fully automated content generation system that showcases the simplicity and power of Convex's backend features.

For the complete code used in this project, check out the GitHub Pull Request. Convex provides an excellent foundation for creating intelligent, scalable apps, and we’re excited to see what you’ll build with it!

Build in minutes, scale forever.

Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started