Skip to main content
This approach gives you full control over prompts in version control while still leveraging Freeplay’s strong associations between prompt versions and observability sessions. This allows you to instantly create datasets from observability, iterate on. prompts, and run online evaluations. You’ll need to create your prompts in Freeplay to obtain a template name and id, then create new versions of the templates as you iterate on them to enable the ability to associate your iterations with prompt performance metrics like evaluations, cost, and latency.

📌 Key Benefits

  • Code-First Workflow: Prompts live in your Git repository
  • Standard Review Process: Use your existing code review workflow for prompt changes
  • Automatic Sync: Push prompts to Freeplay as part of your deployment pipeline
  • Full Flexibility: Programmatically manage all prompt aspects

Best For:

  • Teams who prefer infrastructure-as-code approaches
  • Organizations with strict code review requirements for all changes
  • Developers who want prompts versioned alongside application code

Step 1. Create a Prompt in Freeplay

To get started quickly you can follow the create, iterate, and test prompts in the UI quick start guide to create a prompt in the UI. The integrations page will list the project_id, template_name, and version you will need. A more sustainable route is to use the API to create and update prompt templates as they are modified in code. When you keep Freeplay updated with your most recent prompt changes, Freeplay observability will track deployment changes. Below are examples of how you can accomplish this via the SDKs and API.
import os
import json
import requests
from dotenv import load_dotenv

load_dotenv()

# Configuration

FREEPLAY_API_KEY = os.getenv("FREEPLAY_API_KEY")
project_id = "<YOUR-PROJECT-ID>"

# self hosted instances will need to change the hostname

base_api_url = "https://app.freeplay.ai/api/v2"

template_name = "api-test"

def create_template():
url = (
base_api_url + f"/projects/{project_id}/prompt-templates/name/{template_name}/versions?create_template_if_not_exists=1"
)

    prompt_template_setup = {
        "template_messages": load_prompt_content(prompt_file_path),
        "provider": "openai",
        "model": "gpt-4.1-mini-2025-04-14",
        "llm_parameters": {
            "temperature": 0.2,
            "max_tokens": 256,
        },
        "version_name": "test-version-1",
        "version_description": "Development test version with mustache variable",
    }
    return send_request(url, prompt_template_setup)

def update_template(template_id):
url = (
base_api_url + f"/projects/{project_id}/prompt-templates/id/{template_id}/versions"
)

    # Load the base messages and modify the system message
    messages = load_prompt_content(prompt_file_path)
    for msg in messages:
        if msg["role"] == "system":
            msg["content"] = "You talk like a pirate. " + msg["content"]

    prompt_template_setup = {
        "template_messages": messages,
        "provider": "openai",
        "model": "gpt-4.1-mini-2025-04-14",
        "llm_parameters": {
            "temperature": 0.2,
            "max_tokens": 256,
        },
        "version_name": "test-version-2",
        "version_description": "Update-to-tone",
    }

    return send_request(url, prompt_template_setup)

# Read prompt content from file

def load_prompt_content(filepath): # simulate loading a prompt from file
return [
{
"role": "system",
"content": "You are a helpful LLM assistant. Who always responds with the user's name."
},
{
"role": "user",
"content": "Hi, my name is {{name}}."
}
]

def send_request(url, prompt_template_data):
headers = {
"Authorization": f"Bearer {FREEPLAY_API_KEY}",
"Content-Type": "application/json",
}

    try:
        response = requests.post(url, headers=headers, json=prompt_template_data)
        response.raise_for_status()  # Raise an exception for bad HTTP status codes
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error sending request: {e}")
        return None

if **name** == "**main**": # Create template first
create_response = create_template()
print(create_response)

    # Extract template ID from the create response
    template_id = create_response.get("prompt_template_id")
    template_version_id = create_response.get("prompt_template_version_id")
    print(
        f"created prompt template with template id {template_id} and version id {template_version_id}"
    )

    # If template ID exists, update the template
    if template_id:
        update_response = update_template(template_id)
        print(update_response)

Step 2: Record a session to Freeplay

After acquiring the template id and prompt template version id from the URL in the UI, record a session to Freeplay. Continue building prompts in your application, but link them to Freeplay when logging:
from openai import OpenAI
import os
from freeplay import Freeplay, RecordPayload, CallInfo
from freeplay.resources.prompts import PromptVersionInfo
from google.colab import userdata

## Prepare Clients

# OpenAI

openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# Anthropic

anthropic_client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# Freeplay

# TODO: Set these parameters

project_id="<YOUR-PROJECT-ID>"
base_api_url = "https://app.freeplay.ai/api"

# NOTE: found in the URL of prompt template edit page

# for example https://app.freeplay.ai/projects/<Project_ID>/templates/<Template_ID>/edit/<Version_ID>

template_version_id="<YOUR-TEMPLATE-VERSION-ID>"

fpClient = Freeplay(
freeplay_api_key=os.getenv("FREEPLAY_API_KEY"),
api_base=base_api_url #if self hosted, replace with your instance url
)

# TODO: Set your Provider

provider="openai"

# Load prompt content (simulating loading from file)

def load_prompt_content():
return [
{
"role": "system",
"content": "You talk like a pirate. You are a helpful LLM assistant. Who always responds with the user's name."
},
{
"role": "user",
"content": "Hi, my name is {{name}}."
}
]

# Format messages by replacing mustache variables with input values (can be handled using formatted prompts from Freeplay)

def format_messages(messages, variables):
formatted = []
for msg in messages:
content = msg["content"]
for key, value in variables.items():
content = content.replace("{{" + key + "}}", value)
formatted.append({"role": msg["role"], "content": content})
return formatted

# Define model and prompt variables

prompt_vars = {"name": "Jill"}

# Load and format messages with variables

messages = format_messages(load_prompt_content(), prompt_vars)

if provider == "openai":
model="gpt-4.1-mini-2025-04-14"
response = openai_client.chat.completions.create(
model=model,
messages=messages
) # Add OpenAI response to all messages
all_messages = messages + [{"role": "assistant", "content": response.choices[0].message.content}]
elif provider == "anthropic": # Select the system prompt
system_prompt = messages[0]["content"]
model="claude-sonnet-4-5-20250929"
response = anthropic_client.messages.create(
model=model,
max_tokens=1024,
system=system_prompt,
messages=[
{"role": "user", "content": messages[1]["content"]}
]
) # Add Anthropic response to all messages
all_messages = messages + [{"role": "assistant", "content": response.content[0].text}]

# Record the completion data to Freeplay

fpClient.recordings.create(
RecordPayload(
project_id=project_id, # avaialble on the observability
call_info=CallInfo(provider=provider, model=model),
all_messages=all_messages,
inputs=prompt_vars,
prompt_version_info=PromptVersionInfo(
prompt_template_version_id=template_version_id,
environment="latest",
)
)
)

Additional Recording Functionality

You can also record tool calls, media, and group by traces to improve search, observability and reviews. Learn more in the Recording SDK or API documentation. You can also find additional examples in the common integration options