Prompt engineering

Write clear, reliable prompts for SignalWire AI agents
View as MarkdownOpen in Claude

A prompt tells an AI agent who it is, what it needs to accomplish, and how to handle the conversation. The most effective prompts give the model the context only your application can provide, then leave exact data and hard rules to your code.

Start with the smallest prompt that handles the common case. Add instructions when testing reveals a specific gap, and remove instructions that don’t change the agent’s behavior.

Prompt surfaces

SignalWire AI Agents accept instructions in five places. Put each instruction as close as possible to the behavior it controls.

Prompt surfaceUse it for
Main promptIdentity, objective, stable knowledge, conversation flow, and response style
Context stepsGoals, available functions, and transitions for one stage of a multi-step conversation
SignalWire AI Gateway (SWAIG) functionsWhen to call a function and how to collect its parameters
ConscienceShort, global standards that remain active throughout the conversation
Post-promptSummarizing or extracting data after the conversation ends

The main prompt, context steps, functions, and conscience affect the live conversation. The post-prompt runs after the conversation and cannot change how the agent responds during the call.

Main prompt

The main prompt provides shared context for the complete conversation. Organize it so the agent can distinguish its objective, facts, process, and speaking style. A useful starting structure is:

  • Role and objective: who the agent represents, what it does, and what completes the interaction
  • Stable knowledge: terminology, available services, and information required to complete a task
  • Task structure: the expected flow, written as outcomes rather than a script for every sentence
  • Response guidelines: the tone, response length, and application-specific formatting rules

Keep stage-specific directions in context steps, function selection details in SWAIG function descriptions, global standards in the conscience, and after-call extraction in the post-prompt.

Context steps

Use context steps when a conversation has distinct stages. Each step can define:

  • text: what the agent should accomplish and how it should behave in that stage
  • step_criteria: the condition that completes the stage
  • functions: the functions available during the stage
  • valid_steps: the stages the agent may enter next

For example, expose get_quote only in the quote step. This prevents the agent from quoting before it confirms the trip instead of relying on a sentence that asks it not to.

SWAIG functions

A function definition is also a prompt. Give each function a descriptive name, state when to call it, and describe each parameter precisely.

Prefer get_quote with the description “Price a trip after the caller confirms both addresses” over function_1 with the description “Check price.” Parameter descriptions should also define the expected value, such as an address in the caller’s words or one of the supported vehicle types.

Return enough information for the agent to respond without guessing. A quoting function might return the fare, distance, and a caller-facing explanation when it rejects a trip. A response such as ok leaves the agent to invent the missing details.

Conscience

Use the conscience for a small set of standards that apply throughout the conversation, including after function calls. Write the behavior you want rather than a long list of prohibitions.

For example: “Say what you know, be clear about what you don’t know, and offer to find help instead of filling gaps. Collect only the information needed to serve the caller.”

Exact outcomes still belong in code. A conscience can tell the agent to respect a pricing policy; only the quoting function can enforce that policy on every call.

Post-prompt

Use the post-prompt to summarize the completed conversation or extract structured data for another system. Name the required fields, allowed values, and output format.

For example, ask for a JSON object containing the final confirmed pickup address, destination, and an outcome limited to quoted, declined, or unavailable.

Write focused instructions

Use clear, specific language and consistent headings. Tell the agent what a successful interaction looks like, then give it enough flexibility to handle interruptions, corrections, and information provided out of order.

Describe the behavior you want instead of trying to anticipate every failure. “Transfer fare disputes to a dispatcher” gives the agent a clear action. “Don’t mishandle disputes” does not.

Avoid overprompting

Models already understand grammar, common conversational patterns, and how to structure a sentence. Use prompt space for your application’s vocabulary, workflow, boundaries, and required response format.

Don’t embed large catalogs, rate tables, policies, or facts that change. They make the prompt harder to maintain and can still leave the model to calculate or choose an answer. Retrieve that data with a tool call, then have the agent relay the result.

The complete example applies these principles without putting fare data in the prompt.

Complete example

The two implementations below create the same fare-quote agent. The Server SDK example is first and runs its lookup in Python. The SDK generates a function webhook URL that routes calls to the get_quote handler. In the SWML example, replace the placeholder web_hook_url with an endpoint that performs the same lookup.

Both versions include a main prompt, context steps, a get_quote function, a conscience, and a post-prompt. The small route table stands in for your dispatch system; replace it with your own current data source in production.

1from signalwire import AgentBase, FunctionResult
2
3
4FARES = {
5 ("123 gough street", "sfo"): 38.60,
6 ("123 gough street", "embarcadero"): 10.25,
7}
8
9
10class QuoteAgent(AgentBase):
11 def __init__(self):
12 super().__init__(name="bayview-quote-agent")
13
14 self.prompt_add_section(
15 "Role and objective",
16 "You are Ada, the dispatcher for Bayview Taxi. Give callers fare quotes. "
17 "A call is complete when the caller receives a quote or learns that the "
18 "route is unavailable.",
19 )
20 self.prompt_add_section(
21 "Stable knowledge",
22 "A quote requires a pickup address and destination. Recognize SFO as "
23 "San Francisco International Airport.",
24 )
25 self.prompt_add_section(
26 "Response guidelines",
27 "Use a calm, efficient tone. Keep replies short enough for the caller to "
28 "interrupt. Read the fare exactly as get_quote returns it.",
29 )
30
31 self.set_params(
32 {
33 "conscience": (
34 "Say what you know and be clear about what you don't know. "
35 "Collect only the information needed to quote the trip."
36 )
37 }
38 )
39 self.set_post_prompt(
40 "Return JSON with pickup_address, destination, and outcome. "
41 "Set outcome to quoted, declined, or unavailable."
42 )
43
44 self.define_tool(
45 name="get_quote",
46 description=(
47 "Return the fare after the caller confirms both addresses. "
48 "Call again if either address changes."
49 ),
50 parameters={
51 "type": "object",
52 "properties": {
53 "pickup": {
54 "type": "string",
55 "description": "The confirmed pickup address",
56 },
57 "destination": {
58 "type": "string",
59 "description": "The confirmed destination; use SFO for the airport",
60 },
61 },
62 "required": ["pickup", "destination"],
63 },
64 handler=self.get_quote,
65 )
66
67 contexts = self.define_contexts()
68 default = contexts.add_context("default")
69 default.add_step("collect_trip") \
70 .set_text("Collect and confirm the pickup address and destination.") \
71 .set_step_criteria("Both addresses are confirmed.") \
72 .set_functions([]) \
73 .set_valid_steps(["quote"])
74 default.add_step("quote") \
75 .set_text("Call get_quote, read its result, and ask if the caller needs anything else.") \
76 .set_step_criteria("The caller has received the quote or learned it is unavailable.") \
77 .set_functions(["get_quote"])
78
79 def get_quote(self, args, raw_data):
80 pickup = args.get("pickup", "").lower().strip()
81 destination = args.get("destination", "").lower().strip()
82 fare = FARES.get((pickup, destination))
83
84 if fare is None:
85 return FunctionResult(
86 "That route is unavailable. Say that you cannot quote it and ask "
87 "whether the caller wants to try different addresses."
88 )
89
90 return FunctionResult(
91 f"The fare is ${fare:.2f}. Read this exact amount to the caller."
92 )
93
94
95if __name__ == "__main__":
96 QuoteAgent().run()

Put guardrails in the right layer

A prompt requests behavior; code enforces it. Use these options in order:

  1. Define success. A clear objective handles many boundaries without extra prohibitions.
  2. Enforce exact rules in code. Use functions for prices, discounts, eligibility, permissions, current data, and other outcomes that must be correct.
  3. Remove unavailable actions. Limit functions and transitions with context steps so the agent cannot take an action at the wrong stage.
  4. Apply global standards through the conscience. Reserve it for a short set of behavioral, safety, compliance, or brand standards that code cannot express.
  5. Add a prompt rule when needed. Make it specific and describe the desired response. “Transfer fare disputes to a dispatcher” gives the agent an action; “don’t mishandle disputes” does not.

Test and refine

Treat the first prompt as a draft. Test it against representative conversations, revise one behavior at a time, and keep the prompt under version control with its test cases.

Test the common path first, then cover:

  • interruptions, corrections, and information provided out of order
  • missing, ambiguous, or unsupported requests
  • attempts to override the agent’s instructions
  • function failures and handoffs
  • boundaries where the agent should say it doesn’t know

When a test fails, identify the right layer before adding prompt text. Fix incorrect data or hard rules in code, function timing in context steps, function selection in its description, and global behavior in the conscience.

Trim after each successful revision. Remove one line, rerun the same tests, and keep the deletion if behavior stays the same. This prevents duplicated rules and general knowledge from hiding the instructions specific to your application.

Next steps