[Part-2] Text to Action: Words to Calendar Events
Turn your words into calendar events effortlessly! This AI-powered assistant understands your text, extracts key details, and schedules events for you.
Join the DZone community and get the full member experience.
Join For FreeWelcome back to the “Text to Action” series, where we build intelligent systems that transform natural language into real-world actionable outcomes using AI.
In Part 1, we established our foundation by creating an Express.js backend that connects to Google Calendar’s API. This gave us the ability to programmatically create calendar events through an exposed API endpoint.
Today, we’re adding the AI magic — enabling users to simply type natural language descriptions like “Schedule a team meeting tomorrow at 3 pm” and have our system intelligently transform such words into adding an actual calendar event action.
What We’re Building
We’re bridging the gap between natural human language and structured machine data. This ability to parse and transform natural language is at the heart of modern AI assistants and automation tools.
The end goal remains the same: create a system where users can type or say “create a party event at 5 pm on March 20” and instantly see it appear in their Google Calendar.
This tutorial will show you how to:
- Set up a local language model using Ollama
- Design an effective prompt for text-to-json conversion
- Build a natural language API endpoint
- Create a user-friendly interface for text input
- Handle date, time, and timezone complexities
The complete code is available on GitHub.
Prerequisites
Before starting, please make sure you have:
- Completed Part 1 setup
- Node.js and npm installed
- Ollama installed locally
- The llama3.2:latest model pulled via Ollama
# Install Ollama from https://ollama.com/
# Then pull the model:
ollama pull llama3.2:latest
Architecture Overview
Here’s how our complete system works:
- User enters natural language text through the UI or API call.
- System sends text to Ollama with a carefully designed prompt.
- Ollama extracts structured data (event details, time, date, etc.).
- System passes the structured data to Google Calendar API.
- Google Calendar creates the event.
- User receives confirmation with event details.
The magic happens in the middle steps, where we convert unstructured text to structured data that APIs can understand.
Step 1: Creating the NLP Service
First, let’s install the axios package for making HTTP requests to our local Ollama instance:
npm install axios
Now, create a new file nlpService.js
to handle the natural language processing. Here are the key parts (full code available on GitHub):
const axios = require('axios');
// Main function to convert text to calendar event
const textToCalendarEvent = async (text, timezone) => {
try {
const ollamaEndpoint = process.env.OLLAMA_ENDPOINT || 'http://localhost:11434/api/generate';
const ollamaModel = process.env.OLLAMA_MODEL || 'llama3.2:latest';
const { data } = await axios.post(ollamaEndpoint, {
model: ollamaModel,
prompt: buildPrompt(text, timezone),
stream: false
});
return parseResponse(data.response);
} catch (error) {
console.error('Error calling Ollama:', error.message);
throw new Error('Failed to convert text to calendar event');
}
};
// The core prompt engineering part
const buildPrompt = (text, timezone) => {
// Get current date in user's timezone
const today = new Date();
const formattedDate = today.toISOString().split('T')[0]; // YYYY-MM-DD
// Calculate tomorrow's date
const tomorrow = new Date(today.getTime() + 24*60*60*1000);
const tomorrowFormatted = tomorrow.toISOString().split('T')[0];
// Get timezone information
const tzString = timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
return `
You are a system that converts natural language text into JSON for calendar events.
TODAY'S DATE IS: ${formattedDate}
USER'S TIMEZONE IS: ${tzString}
Given a text description of an event, extract the event information and return ONLY a valid JSON object with these fields:
- summary: The event title
- description: A brief description of the event
- startDateTime: ISO 8601 formatted start time
- endDateTime: ISO 8601 formatted end time
Rules:
- TODAY'S DATE IS ${formattedDate} - all relative dates must be calculated from this date
- Use the user's timezone for all datetime calculations
- "Tomorrow" means ${tomorrowFormatted}
- For dates without specified times, assume 9:00 AM
- If duration is not specified, assume 1 hour for meetings/calls and 2 hours for other events
- Include timezone information in the ISO timestamp
Examples:
Input: "Schedule a team meeting tomorrow at 2pm for 45 minutes"
Output:
{"summary":"Team Meeting","description":"Team Meeting","startDateTime":"${tomorrowFormatted}T14:00:00${getTimezoneOffset(tzString)}","endDateTime":"${tomorrowFormatted}T14:45:00${getTimezoneOffset(tzString)}"}
Now convert the following text to a calendar event JSON:
"${text}"
REMEMBER: RESPOND WITH RAW JSON ONLY. NO ADDITIONAL TEXT OR FORMATTING.
`;
};
// Helper functions for timezone handling and response parsing
// ... (See GitHub repository for full implementation)
module.exports = { textToCalendarEvent };
The key innovation here is our prompt design that:
- Anchors the model to today’s date
- Provides timezone awareness
- Gives clear rules for handling ambiguous cases
- Shows examples of desired output format
Step 2: Calendar Utility Function
Utility module for calendar operations. Here’s the simplified version (utils/calendarUtils.js
):
const { google } = require('googleapis');
// Function to create a calendar event using the Google Calendar API
const createCalendarEvent = async ({
auth,
calendarId = 'primary',
summary,
description,
startDateTime,
endDateTime
}) => {
const calendar = google.calendar({ version: 'v3', auth });
const { data } = await calendar.events.insert({
calendarId,
resource: {
summary,
description: description || summary,
start: { dateTime: startDateTime },
end: { dateTime: endDateTime }
}
});
return {
success: true,
eventId: data.id,
eventLink: data.htmlLink
};
};
module.exports = { createCalendarEvent };
Step 3: Updating the Express App
Now, let’s update our app.js
file to include the new natural language endpoint:
// Import the new modules
const { textToCalendarEvent } = require('./nlpService');
const { createCalendarEvent } = require('./utils/calendarUtils');
// Add this new endpoint after the existing /api/create-event endpoint
app.post('/api/text-to-event', async (req, res) => {
try {
const { text } = req.body;
if (!text) {
return res.status(400).json({
error: 'Missing required field: text'
});
}
// Get user timezone from request headers or default to system timezone
const timezone = req.get('X-Timezone') || Intl.DateTimeFormat().resolvedOptions().timeZone;
// Convert the text to a structured event with timezone awareness
const eventData = await textToCalendarEvent(text, timezone);
const { summary, description = summary, startDateTime, endDateTime } = eventData;
// Create the calendar event using the extracted data
const result = await createCalendarEvent({
auth: oauth2Client,
summary,
description,
startDateTime,
endDateTime
});
// Add the parsed data for reference
res.status(201).json({
...result,
eventData
});
} catch (error) {
console.error('Error creating event from text:', error);
res.status(error.code || 500).json({
error: error.message || 'Failed to create event from text'
});
}
});
Step 4: Building the User Interface
We’ll create a dedicated HTML page for natural language input (public/text-to-event.html
). Here's a simplified version showing the main components:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text to Calendar Event</title>
<!-- CSS styles omitted for brevity -->
</head>
<body>
<div class="nav">
<a href="/">Standard Event Form</a>
<a href="/text-to-event.html">Natural Language Events</a>
</div>
<h1>Text to Calendar Event</h1>
<p>Simply describe your event in natural language, and we'll create it in your Google Calendar.</p>
<div class="container">
<h2>Step 1: Authenticate with Google</h2>
<a href="/auth/google"><button class="auth-btn">Connect to Google Calendar</button></a>
</div>
<div class="container">
<h2>Step 2: Describe Your Event</h2>
<div>
<div>Try these examples:</div>
<div class="example">Schedule a team meeting tomorrow at 2pm for 45 minutes</div>
<div class="example">Create a dentist appointment on April 15 from 10am to 11:30am</div>
<div class="example">Set up a lunch with Sarah next Friday at noon</div>
</div>
<form id="event-form">
<label for="text-input">Event Description</label>
<textarea id="text-input" required placeholder="Schedule a team meeting tomorrow at 2pm for 45 minutes"></textarea>
<button type="submit">Create Event</button>
<div class="loading" id="loading">Processing your request <span></span></div>
</form>
<div id="result"></div>
</div>
<script>
// JavaScript code to handle the form submission and examples
// See GitHub repository for full implementation
</script>
</body>
</html>
The interface provides clickable examples and a text input area and displays results with a loading indicator.
Step 5: Creating a Testing Script
For easy command-line testing to automatically detect your current timezone, here’s a simplified version of our shell script test-text-to-event.sh
:
#!/bin/bash
# Test the text-to-event endpoint with a natural language input
# Usage: ./test-text-to-event.sh "Schedule a meeting tomorrow at 3pm for 1 hour"
TEXT_INPUT=${1:-"Schedule a team meeting tomorrow at 2pm for 45 minutes"}
# Try to detect system timezone
TIMEZONE=$(timedatectl show --property=Timezone 2>/dev/null | cut -d= -f2)
# Fallback to a popular timezone if detection fails
TIMEZONE=${TIMEZONE:-"America/Chicago"}
echo "Sending text input: \"$TEXT_INPUT\""
echo "Using timezone: $TIMEZONE"
echo ""
curl -X POST http://localhost:3000/api/text-to-event \
-H "Content-Type: application/json" \
-H "X-Timezone: $TIMEZONE" \
-d "{\"text\": \"$TEXT_INPUT\"}" | json_pp
echo ""
Don’t forget to make it executable: chmod +x test-text-to-event.sh
If you know your timezone already, you could pass it directly like below
curl -X POST http://localhost:3000/api/text-to-event \
-H "Content-Type: application/json" \
-H "X-Timezone: America/New_York" \
-d '{"text": "Schedule a team meeting tomorrow at 3pm for 1 hour"}'
Step 6: Updating Environment Variables
Create or update your .env
file to include the Ollama settings:
# Google Calendar API settings (from Part 1)
GOOGLE_CLIENT_ID=your_client_id_here
GOOGLE_CLIENT_SECRET=your_client_secret_here
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
PORT=3000
# Ollama LLM settings
OLLAMA_ENDPOINT=http://localhost:11434/api/generate
OLLAMA_MODEL=llama3.2:latest
# Server configuration
PORT=3000
The Magic of Prompt Engineering
The heart of our system lies in the carefully designed prompt that we send to the language model. Let’s break down why this prompt works so well:
- Context setting. We tell the model exactly what we want it to do — convert text to a specific JSON format.
- Date anchoring. By providing today’s date, we ground all relative date references.
- Timezone awareness. We explicitly tell the model what timezone to use.
- Specific format. We clearly define the exact JSON structure we expect back.
- Rules for ambiguities. We give explicit instructions for handling edge cases.
- Examples. We show the model exactly what good outputs look like.
This structured approach to prompt engineering is what makes our text conversion reliable and accurate.
Testing the Complete System
Now that everything is set up, let’s test our system:
- Start the server:
npm run start
- Make sure Ollama is running with the llama3.2:latest model
- Open your browser to
http://localhost:3000/text-to-event.html
- Authenticate with Google (if you haven’t already)
- Enter a natural language description like “Schedule a team meeting tomorrow at 2 pm”
- Click “Create Event” and watch as it appears in your calendar!
You can also test from the command line:
./test-text-to-event.sh "Set up a project review on Friday at 11am"
To test if your Ollama is running as expected, try this test query:
curl -X POST http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:latest",
"prompt": "What is the capital of France?",
"stream": false
}'
The Complete Pipeline
Let’s review what happens when a user enters text like “Schedule a team meeting tomorrow at 2 pm for 45 minutes”:
- The text is sent to our
/api/text-to-event
endpoint, along with the user's timezone - Our NLP service constructs a prompt that includes:
- Today’s date (for reference)
- The user’s timezone
- Clear instructions and examples
- The user’s text
- Ollama processes this prompt and extracts the structured event data:
JSON
{ "summary": "Team Meeting", "description": "Team Meeting", "startDateTime": "2025-03-09T14:00:00-05:00", "endDateTime": "2025-03-09T14:45:00-05:00" }
- Our app passes this structured data to the Google Calendar API
- The Calendar API creates the event and returns a success message with a link
- We display the confirmation to the user with all the details
This demonstrates the core concept of our “Text to Action” series: transforming natural language into structured data that can trigger real-world actions.
Conclusion and Next Steps
In this tutorial, we’ve built a powerful natural language interface for calendar event creation. We’ve seen how:
- A well-crafted prompt can extract structured data from free-form text
- Timezone and date handling requires careful consideration
- Modern LLMs like llama3.2 can understand and process natural language reliably
But we’ve only scratched the surface of what’s possible. In future episodes of the “Text to Action” series, we’ll explore:
- Adding voice recognition for hands-free event creation
- Building agent-based decision-making for more complex tasks
- Connecting to multiple services beyond just calendars
The complete code for this project is available on GitHub.
Stay tuned for Part 3, where we’ll add voice recognition to our system and make it even more accessible!
Resources
Let me know in the comments what you’d like to see in the next episode of this series!
Published at DZone with permission of Vivek Vellaiyappan Surulimuthu. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments