Skip to main content
You have one agent working from Your first agent. This page adds one task to it. Nothing here needs a second agent, a phone number or a new account. Every change below ends with unmute validate, and every change leaves you a package that runs. The example is a parcel tracking line. A caller reads out a tracking number, the agent looks it up and says when the parcel is due. It is small on purpose. The shape is what matters, and you will swap the parcel for your own thing.
Tasks need a code target: Pipecat or LiveKit. The package unmute init writes targets LiveKit, so you are ready.SLNG compiles one agent with one prompt, so it refuses a task today and tells you to fold the step into the agent’s instructions instead. Tasks on SLNG are coming.

What a task is

A task is a smaller conversation inside the call. It has its own prompt and its own tools. To the model, a task is one more entry in its function list, right next to the agent’s tools. When the model calls it, the task’s prompt takes over. When the task is done, the agent that started it, the owner, gets the caller back, along with whatever the task saved. You reach for a task when part of the call needs a narrower prompt, a smaller tool list, or an answer you want to keep. If none of those is true, stay with one agent and its tools. Most agents should. One word to settle before we start. A task is one step of the call. Some of the compiler’s messages say “step” for that reason. On this page the thing you write is always a task. On this page: nine steps, each ending with a package that validates, so you can stop anywhere and come back.
  1. Start with no task - confirm the package you already have still works
  2. Give the agent a tool - a lookup, so there is something to move
  3. Say what you want to keep - declare the variables
  4. Add the task - move the tool onto it
  5. Save the answer - assign:
  6. Read it back - a placeholder in the owner’s prompt
  7. Say what success looks like - finish:, so the tool ends the step
  8. Open with the question - announce: and opening: listen
  9. Run it - talk to it in the browser
1

Start with no task

This is the package from the last page. Make sure it still validates:
One agent, one prompt, one built-in tool so it can hang up. Keep this picture in mind: each change below adds one thing to it.
2

Give the agent a tool

A tool does real work and comes back with a result. This one is a small Python function that stands in for your delivery system. It lives in the package, in two files.The tool file says what the tool takes and what it returns:
tools/look_up_parcel.yaml
The handler is a function with the same name as the tool:
tools/look_up_parcel.py
Look at the status field in output:. It has an enum:, a fixed list of the values it can hold. That list is what will let the task know, later, whether the lookup worked. Write one for any tool a task will end on.Now name the tool in two places in agent.yaml. The agent’s own list says the agent may call it. The top-level list says the tool exists.
agent.yaml
And tell the agent what to do with it, at the end of instructions.md:
instructions.md
This already works, and for many agents it is enough. Stop here if the job is one lookup and one answer. The rest of this page is for when you want the lookup to be its own small conversation, and you want to keep what it found.
3

Say what you want to keep

A task can save what it learns. You say what that is by declaring a variable, once, with a type. Add this block to agent.yaml, anywhere at the top level:
agent.yaml
Date is a built-in type. A value that is not a date in that form is refused where it enters, and the model is told the form, so it can try again. The description is read by the model, so write it for the model.Nothing reads these yet. Declaring a value and using it are two separate things, and that is on purpose.
4

Add the task

A task is written inside the agent that runs it, under tasks:. It has a name, a when: that tells the model when to run it, its own prompt file, and its own tools.Make a tasks/ folder next to agent.yaml and put the task’s prompt in it:
tasks/find-parcel.md
Then add the task to the agent:
agent.yaml
It still validates, and it warns:
Read that warning, because it is the most common mistake with a first task. The model sees one list with the agent’s tools and the task in it, and it takes the shortest route. If the agent can call look_up_parcel itself, it will, and the task never runs. The fix is to move the tool: take it off the agent and leave it on the task.
agent.yaml
The top-level tools: list keeps look_up_parcel. That list says the tool exists; the task now says who may call it.Change the agent’s prompt to match. It no longer looks parcels up itself:
instructions.md
The warning is gone. The model now has one way to look up a parcel, and it goes through the task.
5

Save the answer

Right now the task can find the parcel, but when it ends, what it found is gone. assign: says which values the task saves, and where. Each line is one variable you declared, and the tool result field it takes:
agent.yaml
Here is what that line did. Every task gets a finish call, a function the model calls to say the task is done. assign: gives that call two arguments, tracking_number and delivery_date, typed from the variables they land in. When the model calls finish, each value is checked against its type. If one is wrong, nothing is saved and the task stays open, so a bad date never gets written down.You do not write “call finish when you are done” in the task’s prompt. The compiler adds that rule to the prompt for you. You will see the exact words when you add finish:.A wrong name on the left is refused with the line it is on:
6

Read it back

Saving a value does not show it to anybody. A prompt sees a saved value only when it names it, with the variable in double braces. Add this to the end of instructions.md:
instructions.md
Before the task has run, an empty value renders as none recorded yet., so write the sentence so it still makes sense with those words in it. After the task has run, the owner reads the real values on its next turn and can answer without looking the parcel up again.
7

Say what success looks like

So far the task ends when the model decides to call finish. That is one more model request after the lookup, and one more chance for the model to do something else first. The tool already knows whether it worked: its status is found or not_found. finish: lets that result end the task:
agent.yaml
Read it as a sentence: “this task is finished when look_up_parcel returns status: found”. When that happens, the task saves its assign: straight from the tool result and ends. The model is not asked, and the result never reaches it. When the status is not_found, the result goes back to the model as normal and the task stays open, which is exactly when you want the prompt’s “ask the caller to check the number” line to do its job.Two rules keep this honest, and the compiler holds both. A finish: entry needs a success: check:
And every success: value has to be one the tool declares in its enum:, so a typo is caught here and not on a call that never ends:
This is also where you can see what the compiler adds to the task’s prompt. Run unmute compile my-agent and open build/livekit/agent.py. Under your own instructions you will find these lines:
So the finish call is still there. The model uses it when the caller asks for something the task cannot do, and for a value the task already holds. Your own prompt should not argue with those lines.
8

Open with the question

A task’s first turn is normally a model request that writes an opening line. This task always opens the same way: it asks for the tracking number. Say that line yourself and skip the request:
agent.yaml
announce: is one fixed line, spoken exactly as written. opening: listen says: speak it, then wait for the caller. The line is recorded as the task’s own first turn, so the caller’s answer lands in a conversation the task can see.Update the task’s prompt so it does not ask the question twice:
tasks/find-parcel.md
9

Run it

Your keys from the quickstart are still in .env. When the browser opens, ask where your parcel is and read out one of the two numbers in the handler, “A B one two three four five six”.Watch the dev page while you talk. The task shows up as its own row when the model enters it, then look_up_parcel gets a row of its own, and then the agent answers. If you never see the task’s row, the model went round it. Check two things: that the agent does not hold the task’s tool, and that the when: line describes what the caller actually said. Running it locally explains every row on that page.

The whole file

This is agent.yaml at the end, with the scaffold’s comments taken out. The three lists near the top are the whole change: one tool on the agent, one task nested under it, and the task’s own tool, finish: and assign:.

What you have now

Where to go next

Tasks

Every task key in full: history, what returns, sharing a task between agents.

Task groups

Two or more tasks that have to run in a fixed order.

Making a task actually run

Why the model skips a task, and what to give it so it does not.

Designing declared state

Choosing what to save, and writing prompts that read well when it is empty.