Serverless AI Task Scheduler

Build a REST API that converts a task list into an AI-generated daily schedule.

Introduction

30 Second Summary

Ever stare at a to-do list and wonder how to fit everything into one day? Imagine sending that list to an AI that hands back a prioritized, time-blocked schedule in seconds.

In this project, you will build a serverless microservice on AWS that does exactly that. You'll connect AWS Lambda, Amazon Bedrock, and API Gateway into a live REST API that takes raw tasks and returns an AI-generated daily schedule.

What You'll Build

You send a POST request with your tasks and available hours from any terminal. Seconds later, you get back a structured JSON schedule with priorities, time blocks, and buffer periods.

By the end of this project, you'll have:

  • A live API endpoint that accepts your tasks and available hours, then returns a time-blocked daily schedule generated by AI.
  • An AI that prioritizes your tasks by urgency and slots them into your day with buffer periods between each one.
  • A shareable API URL that lets others generate their own schedules straight from their terminal.
  • Secret Mission: Add scheduling strategy modes like "Deep Work" and "Pomodoro" that change how the AI structures your schedule.

Do I need any prerequisites?

You need an AWS account with billing enabled. You also need the AWS CLI installed and configured with your credentials.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Lambda with a Test Response

Every AI-powered service starts with a compute layer that can receive requests and return responses. Before adding any intelligence, you need to prove this foundation works on its own.

In this step, you'll create an AWS Lambda function in the AWS Management Console, write a simple handler that parses input and returns a test response, and verify the whole thing runs.

In this step, get ready to:
  • Create a Lambda function named task-scheduler with a Python 3.13 runtime.
  • Write a handler that receives a task list and returns a JSON response.
  • Test the function with a sample payload and confirm a 200 response.
Create the Lambda function

Lambda lets you run code without provisioning or managing servers. You give it a function, and AWS handles everything else.

  • Sign in to the AWS Management Console.
  • In the search bar at the top, type Lambda and select Lambda from the results.
  • Click Create function.
  • Select Author from scratch.
  • In the Function name field, enter task-scheduler.
  • For Runtime, select Python 3.13.
  • Leave Permissions at the default (Create a new role with basic Lambda permissions).
  • Click Create function.

You should see your function's configuration page load with a success banner at the top.

What is an execution role?

When you created the function, AWS automatically created an IAM execution role with basic permissions. This role controls what AWS services your Lambda function is allowed to use.

Right now, the role only has permission to write logs to CloudWatch. In a later step, you'll see exactly what happens when the function tries to use a service it doesn't have permission for.

Replace the default code with your handler

Lambda created a default "Hello world" function for you. You need to replace it with a handler that can parse incoming task data and return a structured response.

  • Select the Code tab.
  • In the code editor, click on the lambda_function.py file to open it.
  • Select all the existing code and delete it.
  • Paste the following code into the editor:
import json

def lambda_handler(event, context):
    # Parse the request body (handles both API Gateway and direct test formats)
    try:
        body = json.loads(event.get("body", "{}"))
    except (json.JSONDecodeError, TypeError):
        body = event

    # Extract the task list and available hours from the request
    tasks = body.get("tasks", [])
    hours = body.get("hours", 8)

    # Return a hardcoded test response with the received input
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": "Test response", "tasks_received": tasks, "hours": hours}),
    }

What does this code do?

  • lambda_handler is the entry point Lambda calls every time the function is invoked. It receives an event (the incoming data) and a context object (metadata about the invocation).
  • The try/except block handles two formats: when API Gateway sends a request (the data arrives as a JSON string inside a body key), and when you test directly in the console (the data IS the event itself).
  • The function extracts tasks and hours from the request, defaulting to an empty list and 8 hours if they're missing.
  • It returns a hardcoded test response with a 200 status code. In a later step, you'll replace this with a real AI-generated schedule.
  • In the DEPLOY section above the editor, click Deploy to save and deploy your code.

You should see a notification confirming your changes were deployed.

Don't see the Deploy button?

  • Make sure you're on the Code tab of your function. The DEPLOY section sits above the code editor.
  • If the lambda_function.py tab is not visible in the editor, look for it in the file explorer panel on the left side of the code editor and click it.

Help me find the Deploy button in the Lambda console.

✔️ Awesome, I've got everything!

Great. Make sure you've saved your file by clicking Deploy before moving on.

ⓧ I'd like to double check the full code

Here's the complete lambda_function.py at this point. Compare it with what you have in the editor:

import json

def lambda_handler(event, context):
    try:
        body = json.loads(event.get("body", "{}"))
    except (json.JSONDecodeError, TypeError):
        body = event

    tasks = body.get("tasks", [])
    hours = body.get("hours", 8)

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": "Test response", "tasks_received": tasks, "hours": hours}),
    }
Test the function with a sample payload

Your code is deployed, but you haven't proven it actually works yet. You'll create a test event that mimics the format API Gateway will use when real requests come in.

  • Click the Test tab next to the Code tab.
  • Under Test event, select Create new event.
  • In the Name field, enter testSchedule.
  • Replace the default JSON in the text box with the following test payload:
{
  "body": "{\"tasks\": [\"Write report\", \"Team standup\", \"Code review\"], \"hours\": 8}"
}

Why is the body a JSON string inside JSON?

When API Gateway forwards a request to Lambda, it wraps the POST body as a JSON string inside a body key. Your handler's json.loads(event.get("body", "{}")) line parses that string back into a Python dictionary.

By formatting your test event this way, you're testing the exact path real requests will take in Step 5.

  • Click Save changes to save the test event.

Before you run the test, think about what you expect to see. Will the function return your three tasks? What status code should it send back?

  • Click the Test button to invoke your function.
  • Under Execution result, expand Details to see the full response.

You should see a response with statusCode: 200 and a body containing your test message, including the three tasks you sent.

Seeing an error instead of a 200 response?

  • Check that you clicked Deploy before running the test. Undeployed code will not execute.
  • Verify that your test event JSON is valid. The escaped quotes inside the body string are easy to mistype. Copy the test event JSON from above and paste it fresh.
  • Make sure your function is named task-scheduler and uses the Python 3.13 runtime.

Help me debug my Lambda test error.

Your compute layer is working. The function receives input, parses it, and returns a structured response. Next up, you'll connect it to Amazon Bedrock so it returns a real AI-generated schedule instead of a hardcoded message.

Connect Lambda to Bedrock

Your Lambda function works. It receives a request, parses the tasks, and returns a response. The problem is that response is hardcoded. It does not actually generate a schedule.

In this step, you wire the function to Amazon Bedrock so it can call a foundation model and generate real schedules from the tasks it receives.

In this step, get ready to:
  • Update the Lambda code to call Amazon Bedrock's Nova Lite model.
  • Increase the function timeout to handle AI model calls.
  • Run a test and observe the result.
Update the Lambda function code

You need to replace the entire function code with a new version that calls Amazon Bedrock instead of returning a hardcoded response.

  • Switch back to the task-scheduler function in the Lambda console.
  • Click the Code tab.
  • In the code editor, select all the existing code in lambda_function.py and delete it.
  • Paste the following code to set up the Bedrock client and input handling:
import json
import boto3

# Create a Bedrock Runtime client to call foundation models
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL_ID = "amazon.nova-lite-v1:0"


def lambda_handler(event, context):
    # Parse the incoming request body
    try:
        body = json.loads(event.get("body", "{}"))
    except (json.JSONDecodeError, TypeError):
        body = event

    tasks = body.get("tasks", [])
    hours = body.get("hours", 8)

    # Return an error if no tasks were provided
    if not tasks:
        return {
            "statusCode": 400,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"error": "No tasks provided. Send a 'tasks' array."}),
        }

What does this code do?

The first three lines import boto3, the AWS SDK for Python, and create a client for the bedrock-runtime service. This client is what your function uses to send prompts to a foundation model.

MODEL_ID stores the identifier for Amazon Nova Lite, the model you will use to generate schedules. The rest of the code parses the incoming request and returns an error if no tasks are provided.

  • Directly below the code you just pasted (still inside lambda_handler), paste this code to build the prompt:
    # Format each task as a bullet point for the prompt
    task_list = "\n".join(f"- {task}" for task in tasks)

    # Build a prompt that tells the model exactly how to create a schedule
    prompt = f"""You are a productivity scheduling assistant. Create a time-blocked daily schedule.

INPUT:
Tasks:
{task_list}

Available hours: {hours}

RULES:
1. Prioritize tasks by implied urgency and importance.
2. Add 10-minute buffer periods between tasks.
3. Schedule the most demanding tasks during morning hours.
4. Return ONLY valid JSON, no other text.

OUTPUT FORMAT:
{{
  "schedule": [
    {{
      "start": "09:00",
      "end": "10:30",
      "task": "Task name here",
      "priority": "high",
      "notes": "Brief scheduling rationale"
    }}
  ],
  "summary": "Brief overview of the schedule"
}}"""

What does this prompt do?

This is prompt engineering in action. Instead of just saying "make me a schedule," you give the model structured rules: prioritize by urgency, add buffer time, and front-load demanding tasks.

The double curly braces {{ and }} are how Python f-strings escape literal curly braces. Without them, Python would try to interpret the JSON example as a variable.

  • Paste the final section of code directly below the prompt to send it to Bedrock and return the response:
    # Structure the request in Nova Lite's expected format
    request_body = {
        "messages": [
            {
                "role": "user",
                "content": [{"text": prompt}],
            }
        ],
        "inferenceConfig": {
            "maxTokens": 1024,
            "temperature": 0.7,
            "topP": 0.9,
        },
    }

    # Send the prompt to Bedrock and extract the generated text
    response = bedrock.invoke_model(
        modelId=MODEL_ID, body=json.dumps(request_body)
    )
    model_response = json.loads(response["body"].read())
    schedule_text = model_response["output"]["message"]["content"][0]["text"]

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": schedule_text,
    }

What does this code do?

The request_body dictionary wraps your prompt in the format Amazon Nova Lite expects: a messages array with a user role, plus an inferenceConfig that controls how the model generates text.

invoke_model sends this request to Bedrock. The response comes back as a stream, so response["body"].read() reads it into a string. The generated schedule lives nested inside the response at output > message > content > text.

  • Click Deploy in the DEPLOY section above the code editor to save your changes.

You should see a banner confirming the function was updated successfully.

✔️ Awesome, I've got everything!

Great. Make sure you see the success banner confirming your code was deployed before moving on.

ⓧ I'd like to double check the full code

Your lambda_function.py should contain three sections, in this order:

  • The imports, Bedrock client, and lambda_handler function start (through the if not tasks error check).
  • The task_list formatting and prompt f-string with scheduling rules and output format.
  • The request_body dictionary, invoke_model call, response parsing, and the final return statement.

Scroll up and confirm each section is present in your code editor, in the right order.

Seeing a syntax error after deploying?

Check that all three code sections are pasted in the correct order with proper indentation. Everything inside lambda_handler should be indented by 4 spaces.

Also make sure you did not accidentally leave any of the old code from the previous step. The entire file should start with import json and import boto3.

Still stuck? Help me fix a syntax error in my Lambda function code

Increase the Lambda timeout

Your Lambda function's default timeout is 3 seconds. Bedrock model calls typically take several seconds to generate a response, so the function would time out before Bedrock finishes.

  • Click the Configuration tab at the top of your function page.
  • Click General configuration in the left sidebar.
  • Click Edit.
  • Change the Timeout value to 0 minutes and 30 seconds.
  • Click Save.

The General configuration panel should now show Timeout as 0 min 30 sec.

Why 30 seconds?

Foundation model calls involve sending your prompt to the model, the model generating tokens one by one, and streaming the result back. This can take anywhere from 3 to 15 seconds depending on the prompt length and response size.

Setting the timeout to 30 seconds gives plenty of headroom without leaving the function running indefinitely if something goes wrong.

Run the test event

Your code is deployed and the timeout is set. Before you run the test, take a moment to think about what you expect to happen.

  • Click the Code tab to return to the code editor.

Your test event from the previous step is still configured with sample tasks and hours.

Predict before you run

Do you think this test will return a generated schedule? Or will something go wrong? Think about what your Lambda function needs in order to successfully call Bedrock.

  • In the TEST EVENTS section, click the run icon next to your test event.

The function fails with an AccessDeniedException error.

This error is expected

Your code is correct. The AccessDeniedException means your function successfully reached Amazon Bedrock, but the IAM execution role attached to your Lambda does not have permission to call bedrock:InvokeModel.

The execution role was created with only basic Lambda permissions (writing logs to CloudWatch). Nobody told it that this function also needs to talk to Bedrock. This is least-privilege in action: AWS services deny everything by default until you explicitly grant access.

Your code works, but your permissions do not. In the next step, you fix the IAM role and see your first AI-generated schedule.

Fix IAM and Get Your First AI Schedule

In the last step, your Lambda function tried to call Amazon Bedrock and got rejected. The AccessDeniedException proved something important: your code is correct. It reached Bedrock, assembled the right request, and was turned away only because the IAM execution role does not have permission to invoke the model.

Now you fix the root cause by adding a least-privilege IAM policy that grants your Lambda exactly one permission: the ability to invoke the Amazon Nova Lite model. The moment you fix it and rerun your test, you will see your first AI-generated daily schedule.

In this step, get ready to:
  • Add a least-privilege inline IAM policy to the Lambda execution role.
  • Rerun the test event and see your first AI-generated schedule.
  • Review the schedule output and notice formatting inconsistencies.
Add Bedrock permissions to the Lambda execution role

Your Lambda function's execution role currently only has basic permissions to write logs to CloudWatch. You need to add a single permission that lets it call the Bedrock model.

  • In the Lambda console, make sure you are on the task-scheduler function page.
  • Click the Configuration tab, then click Permissions in the left sidebar.
  • Under Execution role, click the role name link. This opens the role in the IAM console in a new tab.

What is an execution role?

Every Lambda function has an execution role that controls what AWS services it can access. When you created the function, AWS gave it a role with only AWSLambdaBasicExecutionRole attached. That policy allows writing logs but nothing else.

To call Bedrock, you need to add the bedrock:InvokeModel permission. You will scope it to only the Nova Lite model, following the principle of least privilege.

  • In the IAM console (the new tab), find the Permissions policies section.
  • Click Add permissions, then select Create inline policy.
  • In the Policy editor, select JSON to switch to the JSON editor.
  • Delete any default content in the editor, then paste this policy:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock:InvokeModel",
      "Resource": "arn:aws:bedrock:*::foundation-model/amazon.nova-lite-v1:0"
    }
  ]
}

What does this policy do?

This policy grants a single permission: bedrock:InvokeModel. The Resource field scopes it to only the amazon.nova-lite-v1:0 foundation model.

The * in the region position means the function can invoke this model in any region. The execution role cannot invoke any other Bedrock model or perform any other Bedrock action. This is least-privilege in action.

  • Click Next to proceed to the review page.
  • Under Policy details, enter BedrockInvokeAccess in the Policy name field.
  • Click Create policy.

You should see the new BedrockInvokeAccess policy appear in the role's Permissions policies list, alongside the existing AWSLambdaBasicExecutionRole policy.

Don't see the Create inline policy option?

Make sure you clicked the role name link under Execution role in the Lambda console, not the Edit button. The Edit button lets you swap roles, not modify the current role's permissions.

If the Add permissions button is greyed out, your AWS account may not have sufficient IAM privileges. Help me troubleshoot my IAM permissions for adding an inline policy to a Lambda execution role.

Run the test event and see your first AI schedule

The permission gap is closed. Time to see if the fix worked.

  • Switch back to the Lambda console tab with your task-scheduler function.
  • Click the Code tab to return to the code editor.

Before you run the test, think about what you expect to see. The last time you ran this, you got an AccessDeniedException. You just added bedrock:InvokeModel permission. Do you think the test will succeed this time, or could something else go wrong?

  • Click Test to run your test event.

This time the function succeeds. You should see a 200 status code and a response body containing an AI-generated daily schedule. Amazon Nova Lite took your tasks, prioritized them, and slotted them into time blocks.

Still seeing AccessDeniedException?

IAM policy changes can take a few seconds to propagate. Wait 10-15 seconds and click Test again.

If it still fails, go back to the IAM tab and confirm the inline policy JSON matches exactly. Check that Action is bedrock:InvokeModel (not bedrock:Invoke) and Resource includes amazon.nova-lite-v1:0. Help me fix my Bedrock IAM permissions so my Lambda function can invoke the Nova Lite model.

Review the generated schedule

Read through the schedule in the response body. You will see time blocks with tasks assigned to different slots across the day.

  • Look at how the model structured the output. Is it valid JSON? Are the time blocks consistent?
  • Click Test again to run a second invocation with the same input.
  • Compare the two responses. Notice how the structure might differ between runs. One run might use bullet points, another might use a JSON array, and another might include extra commentary outside the JSON.

Why is the output inconsistent?

The current prompt asks the model to return JSON, but it does not enforce a strict schema or set tight constraints on the output format. Foundation models are probabilistic, so each response varies.

In the next step, you will engineer a better prompt with explicit schema instructions and lower the temperature setting to get reliable, structured output every time.

Your Lambda function now calls Bedrock and generates real AI-powered schedules. Next up, you will tighten the prompt so the output is consistently structured JSON that a frontend could actually parse.

Engineer the Prompt for Structured Output

In the last step, you fixed the IAM permissions and saw your first AI-generated schedule from Amazon Bedrock. Your Lambda function now calls Amazon Nova Lite and returns a response.

But if you ran the test a few times, you probably noticed the output is unpredictable. Sometimes you get clean JSON. Sometimes you get bullet points or paragraphs. A frontend trying to parse this response would break on the inconsistent formats. The fix is tuning the model's inference settings so it produces reliably structured output.

In this step, get ready to:
  • Review the prompt that shapes the AI's response structure.
  • Lower the temperature setting for more deterministic output.
  • Test the function multiple times to verify consistent JSON output.
Review your prompt structure

Your lambda_function.py already has a detailed prompt that tells the model exactly how to format its response. Before changing anything, take a moment to understand why this prompt is structured the way it is.

  • Switch back to the task-scheduler function in the Lambda console.
  • In the Code source editor, scroll down to the prompt variable in lambda_function.py.

What does this prompt do?

The prompt has three key sections that shape the AI's output:

  • INPUT injects the learner's actual tasks and available hours into the prompt. The model sees real data, not generic instructions.
  • RULES tells the model to prioritize by urgency, add 10-minute buffers, schedule demanding tasks in the morning, and return only valid JSON. Each rule constrains the model's behavior.
  • OUTPUT FORMAT gives the model an exact JSON schema to follow. Showing the model the shape of the expected output is one of the most effective prompt engineering techniques.

The prompt itself is solid. So why is the output still inconsistent? The answer is in the inference settings.

Lower the temperature for consistent output

Temperature controls how much randomness the model uses when choosing its next word. A higher value means more creative variation. A lower value means the model sticks closer to the most probable response.

  • In lambda_function.py, scroll down to the inferenceConfig dictionary near the bottom of your code.
  • Find this line:
            "temperature": 0.7,
  • Change 0.7 to 0.3, so the line reads:
            "temperature": 0.3,

What does temperature do?

At 0.7, the model has enough freedom to vary its formatting between runs. It might output JSON one time and markdown the next. At 0.3, the model strongly favors the most likely pattern, which your prompt has defined as structured JSON.

For creative writing, higher temperatures produce more interesting results. For structured data generation, lower temperatures produce more reliable results.

  • In the DEPLOY section above the editor, click Deploy to update your function's code.

Don't see the Deploy button?

Make sure you are on the Code tab of your task-scheduler function. The Deploy button only appears in the Code source editor panel.

Help me find the Deploy button in the Lambda console.

Test for consistent JSON output

The real test of prompt engineering is consistency across multiple runs. A single successful result could be luck. Multiple consistent results prove the prompt and settings work together.

Before you run the test, think about this: with the temperature lowered from 0.7 to 0.3, do you expect the output format to stay the same across runs, or still vary?

  • Click Test to run your saved test event.
  • Under Execution result, expand Details to see the response.

You should see a statusCode of 200 and a response body containing structured JSON with a schedule array and a summary field.

  • Click Test two more times, checking the response after each run.

Each time, the response should follow the same JSON structure: an array of time blocks with start, end, task, priority, and notes fields. The actual task assignments and times may shift slightly between runs, but the format stays consistent.

Why isn't the output identical each run?

Temperature 0.3 is not 0.0. The model still has a small amount of randomness, which means the specific time slots and scheduling rationale may differ slightly. The structure stays the same because the prompt's output format example anchors the model's formatting decisions.

Still seeing unstructured output?

  • Confirm you changed temperature to 0.3 and not topP. Both are in the same inferenceConfig dictionary.
  • Make sure you clicked Deploy after editing the code. Testing without deploying runs the old version.
  • Check that maxTokens is set to 1024. A smaller value might cut off the JSON mid-response.

Help me debug my Lambda function's unstructured output.

Your Lambda now returns reliably structured JSON schedules. The prompt defines the shape and the low temperature keeps the model on track. Next up, you'll expose this function as a public REST API so anyone can send tasks and get a schedule back.

Expose the API with API Gateway

In the last step, you refined your prompt so the Lambda function returns clean, structured JSON schedules every time. Now there is one problem left: the only way to reach your function is through the Lambda console test button.

Nobody outside AWS can send it tasks and get back a schedule. In this step, you create a public REST API using Amazon API Gateway so anyone with the URL can POST tasks and receive an AI-generated schedule. This is the final piece that turns your Lambda function into a complete serverless microservice.

In this step, get ready to:
  • Create a REST API in API Gateway with a /schedule resource and a POST method.
  • Deploy the API to a prod stage and copy the invoke URL.
  • Test the full flow from your terminal using curl.
Create the REST API

API Gateway is the front door for your Lambda function. You will create a REST API, add a resource path, and wire a POST method to your Lambda function using Lambda proxy integration.

  • In the AWS Console search bar at the top, type API Gateway and select the service.
  • Choose Create API.
  • Find REST API (not REST API Private) and choose Build.
  • For API name, enter TaskSchedulerAPI.
  • Keep API endpoint type set to Regional.
  • Choose Create API.

You land on the Resources page showing only the root / resource.

What is a REST API in API Gateway?

A REST API in API Gateway acts as a managed front door for your backend. It handles routing incoming HTTP requests to the right backend service (in your case, a Lambda function) and returns the response to the caller.

Create the /schedule resource and POST method

Your API needs a specific path that callers will hit. You will create a /schedule resource, then attach a POST method that forwards requests to your Lambda function.

  • Select the / root resource in the resource tree.
  • Choose Create resource.
  • For Resource name, enter schedule.
  • Choose Create resource.

You should see /schedule appear under the / resource in the tree.

  • Select the /schedule resource you just created.
  • Choose Create method.
  • For Method type, select POST.
  • For Integration type, select Lambda function.
  • Turn on Lambda proxy integration.
  • For Lambda function, enter task-scheduler and select your function from the dropdown.
  • Choose Create method.

A dialog may appear asking you to grant API Gateway permission to invoke your Lambda function. Choose OK to confirm.

What is Lambda proxy integration?

With Lambda proxy integration, API Gateway passes the entire incoming HTTP request (headers, query strings, body) straight to your Lambda function as the event object. That is why your lambda_handler code parses event.get("body").

Without proxy integration, you would need to configure mapping templates to transform the request before it reaches Lambda. Proxy integration skips all of that.

Don't see your Lambda function in the dropdown?

Make sure the region selector in the top-right corner of the console matches the region where you created your task-scheduler function. API Gateway only lists Lambda functions in the same region.

Help me connect my Lambda function to API Gateway.

Deploy the API and test with curl

Your API exists but is not yet reachable from the outside. You need to deploy it to a stage, which gives it a public invoke URL.

  • In the Resources pane, choose Deploy API.
  • For Stage, select New stage.
  • For Stage name, enter prod.
  • Choose Deploy.

You land on the Stage page. Your invoke URL is displayed under Stage details.

  • Under Stage details, click the copy icon next to the invoke URL to copy it.

What is a stage?

A stage is a named snapshot of your deployed API. The stage name becomes part of the URL (for example, /prod/schedule). Teams often create separate stages like dev, staging, and prod to manage different environments.

Now for the moment of truth. Before you run the test, think about what you expect to happen when you send a POST request with a list of tasks. Do you think the response will come back instantly, or take a few seconds?

  • Open a terminal on your machine.
  • Send a POST request to your API by running this command, replacing the URL with your invoke URL:
curl -X POST [[API_URL="your invoke URL"]]/schedule \
  -H "Content-Type: application/json" \
  -d '{"tasks":["Write report","Team standup","Code review"],"hours":8}'

What does this command do?

  • curl -X POST sends an HTTP POST request to your API Gateway endpoint.
  • -H "Content-Type: application/json" tells the server you are sending JSON data.
  • -d provides the JSON body with three tasks and 8 available hours.
  • /schedule at the end of the URL targets the resource you created in API Gateway.

After a few seconds, you see a structured JSON schedule in your terminal. The AI prioritized your tasks by urgency and slotted them into time blocks with buffer periods.

Getting an error instead of a schedule?

If you see Missing Authentication Token, double-check that you appended /schedule to the end of your invoke URL. The root URL alone does not have a method attached.

If you see Internal Server Error, return to the Lambda console and check the Monitor tab for recent errors. The most common cause is a timeout. Make sure your Lambda function timeout is set to 30 seconds.

If the response takes longer than 29 seconds, API Gateway times out before Lambda finishes. The API Gateway default timeout is 29 seconds, so your Lambda must respond within that window.

Help me troubleshoot my API Gateway response.

Your serverless AI task scheduler is live. Anyone with that URL can send a list of tasks and get back a prioritized, time-blocked schedule powered by Amazon Bedrock. Up next, you can take on the Secret Mission and add multiple scheduling strategies.

Secret mission

Add Scheduling Strategy Modes

Your scheduler works, but it only has one way to organize a day. What if different people work best with different methods? In this secret mission, you add a strategy selector so users can choose between balanced, deep-work, and pomodoro scheduling modes.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses serverless resources that incur zero cost at rest. You only pay for Amazon Bedrock tokens when someone hits the API.

Resources you used:

  • AWS Lambda function (task-scheduler). No cost within free tier (1M requests/month).
  • API Gateway REST API (TaskSchedulerAPI). No cost within free tier (1M calls/month for 12 months).
  • IAM execution role with inline policy granting bedrock:InvokeModel. No cost.
  • Amazon Bedrock usage. Pay-per-token. Amazon Nova Lite pricing is $0.06 per 1M input tokens and $0.24 per 1M output tokens. Your test invocations likely cost under $0.01 total.
  • CloudWatch Logs (/aws/lambda/task-scheduler). Generated automatically by Lambda. Free tier includes 5GB ingestion/month.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep the API available.

  • The serverless architecture means zero cost when nobody is hitting the API. You only pay for Bedrock tokens when a request comes in.
  • Lambda, API Gateway, IAM, and CloudWatch Logs all sit within free-tier limits for this project.
  • It is safe to leave everything deployed.

Pause - I'll come back to this later

Serverless resources incur no cost at rest. There is no pause mechanism needed.

  • Your Lambda function and API Gateway only run when a request arrives. No request means no cost.
  • This is functionally identical to "Keep everything running" for serverless resources.

Delete - I don't want to use this again

Remove all project resources and start fresh. Run these commands in your terminal to fully clean up.

  • Delete the Lambda function by running:
aws lambda delete-function --function-name task-scheduler
  • Find your API Gateway REST API ID by running:
aws apigateway get-rest-apis --query "items[?name=='TaskSchedulerAPI'].id" --output text
  • Copy the ID from the output, then delete the API by running:
aws apigateway delete-rest-api --rest-api-id [[API_ID="your REST API ID from the previous command"]]
  • Delete the CloudWatch log group by running:
aws logs delete-log-group --log-group-name /aws/lambda/task-scheduler
  • Delete the IAM execution role. Open the IAM console, click Roles in the sidebar, and search for task-scheduler.
  • Select the role (it starts with task-scheduler-role-), then click Delete.
  • Confirm the deletion when prompted. The console automatically removes the inline policy and any attached managed policies.

Nice Work!

Nice Work!

Nice work! You just built and deployed a serverless AI microservice that turns a raw task list into an intelligent, time-blocked daily schedule.

You've learned how to:

  • Built a Lambda function in Python that calls Amazon Bedrock's Nova Lite model to generate AI-powered daily schedules, and engineered prompts that produce consistent, structured JSON output.
  • Diagnosed and fixed an IAM AccessDeniedException by applying a least-privilege inline policy scoped to a single foundation model with bedrock:InvokeModel.
  • Exposed the Lambda as a public REST API using API Gateway with Lambda proxy integration and tested the complete serverless microservice end-to-end using curl from the terminal.
  • Secret Mission: Added configurable scheduling strategies (balanced, deep-work, pomodoro) that dynamically alter AI behavior through prompt engineering.

Ready to quiz yourself?