Prompt Management
iOS SDK · Android SDK · Dashboard
Prompt Management lets you store your prompts on the MAIG server and sync them to your app on demand — no app release required to update a prompt. Prompts are versioned, diffed efficiently on the server, and cached locally on device so they are always available at inference time without a network round-trip.
How it works
-
Create prompts in the dashboard
Go to Prompts in the dashboard sidebar. Create a named prompt set and add one or more messages. Each message has a
role(system,user, orassistant) and acontentstring — the same format as the OpenAI messages array. -
Sync at app launch
Call
PromptStore.sync()when your app starts. The SDK sends a hash map of everything it already has cached; the server returns only the prompts that have changed since the last sync. On a cold start the full set is returned; on subsequent syncs only the delta is transmitted. -
Use at inference time
Call
getPrompt(named:)(iOS) orgetPrompt(name)(Android) to retrieve the cached messages for a prompt by name. This is a synchronous, in-memory read — no network call. Prepend or append the messages to your conversation before callinggenerateTextorstreamText.
Prompt sets
A prompt set is the unit of storage. It has:
- A human-readable name — lowercase letters, numbers, and hyphens, up to 64 characters. Names are unique within a project and immutable after creation. To rename, delete and recreate.
- An ordered array of messages, each with a
roleandcontent. - A semantic version (e.g.
v1.2) — major and minor, described below. - A content hash (SHA-256 of the messages) used for efficient delta sync.
Total message content per prompt set is capped at 32 KB. The number of prompt sets per project depends on your plan — see the pricing page for limits. Exceeding the content cap returns 400 prompt_too_large; exceeding your plan's prompt count returns 402 prompt_limit_reached.
Semantic versioning
Every prompt is versioned as major.minor, starting at v1.0. The MAIG server automatically determines which kind of bump to apply each time you save:
| Change made | Bump type | Example |
|---|---|---|
Added a new {{VARIABLE}} to any message | Major | v1.2 → v2.0 |
| Edited message content (no new variable) | Minor | v1.2 → v1.3 |
| Added a new message (no new variable) | Minor | v1.2 → v1.3 |
| Removed a message | Minor | v1.2 → v1.3 |
| Removed a variable from a message | Minor | v1.2 → v1.3 |
| Activated (restored) an older version | Major or minor | Same rules apply to the diff |
Why it matters: A major bump signals that your app code needs to change — specifically, it must now supply a new variable to getPrompt(named:variables:). A minor bump is always safe to receive: no new placeholders, no broken substitution.
Example timeline
v1.0 — initial prompt, variable: {{USER}}
v1.1 — edited tone of the system message (no new variables)
v1.2 — added a second message (no new variables)
v2.0 — added {{PRODUCT}} to the system message ← major bump
v2.1 — minor wording tweak
iOS — PromptStore
Initialization
Create a PromptStore with the same project API key used for AIGatewayClient. You typically create one instance at the app level:
import AIGatewaySDK
let store = PromptStore(apiKey: "maig_YOUR_PROJECT_KEY")
Syncing
Call sync() once at app launch. It fetches any changed prompts and updates the local cache atomically:
// In your App or AppDelegate
Task {
do {
try await store.sync()
} catch {
// Non-fatal — cached prompts from the last successful sync are still available
print("Prompt sync failed: \(error)")
}
}
sync() fails (e.g. no network), getPrompt(named:) will still return the last successfully synced version.
Using prompts at inference time
Retrieve the cached messages for a prompt by name and prepend them to your conversation:
// getPrompt(named:) is a synchronous in-memory read — no network call
let storedMessages = store.getPrompt(named: "support-bot-system") ?? []
let userMessage = Message.user("My order hasn't arrived.")
let messages = storedMessages + [userMessage]
let result = try await client.generateText(messages: messages)
getPrompt(named:) returns nil if the named prompt does not exist or sync() has never completed successfully. Always provide a fallback (e.g. an empty array or a hardcoded default) for the first launch before a sync has occurred.
API reference
public final class PromptStore {
/// Initialize with your project API key.
public init(apiKey: String, baseURL: URL = URL(string: "https://api.maig.dev")!)
/// Fetch changed prompts from the server and update the local cache.
/// Safe to call at app launch or on demand.
public func sync() async throws
/// Return the cached messages for a prompt by name.
/// Returns nil if the prompt has never been synced.
public func getPrompt(named name: String) -> [Message]?
}
Android — PromptStore
Initialization
Create a PromptStore with your project API key and an Android Context. A single instance per project is sufficient:
import com.maig.sdk.PromptStore
val store = PromptStore(
apiKey = "maig_YOUR_PROJECT_KEY",
context = applicationContext,
)
Syncing
sync() is a suspend function. Call it from a coroutine at app launch:
// In Application.onCreate() or a ViewModel init block
lifecycleScope.launch {
try {
store.sync()
} catch (e: Exception) {
// Non-fatal — last synced cache is still available
Log.w("PromptStore", "Sync failed: ${e.message}")
}
}
filesDir) and survives process restarts. If sync() fails, getPrompt() returns the last successfully synced version.
Using prompts at inference time
// getPrompt() is a synchronous in-memory read — no network call
val storedMessages = store.getPrompt("support-bot-system") ?: emptyList()
val messages = storedMessages + Message(role = "user", content = "My order hasn't arrived.")
val result = client.generateText(messages = messages)
getPrompt() returns null if the named prompt does not exist or sync() has never completed successfully. Provide a fallback for the first launch before a sync has occurred.
API reference
class PromptStore(
apiKey: String,
context: Context,
baseUrl: String = "https://api.maig.dev",
) {
/** Fetch changed prompts from the server and update the local cache. */
suspend fun sync()
/** Return the cached messages for a prompt by name, or null if not yet synced. */
fun getPrompt(name: String): List<Message>?
}
Dashboard: managing prompts
All prompt management is done from the Prompts page in the dashboard:
- Create — enter a name and at least one message. The name is permanent; use hyphens to separate words (e.g.
support-bot-system). The prompt starts at v1.0. - Edit — modify message roles or content and click Save. The dashboard automatically computes a major or minor version bump based on whether a new variable was introduced. The version badge updates and client apps receive the new content on their next
sync()call. - Delete — removes the prompt set. Client apps remove it from their local cache on the next
sync()call.
Version history & activation
On Starter, Pro, and Business plans, every save is automatically snapshotted. You can view the full history of a prompt, preview any past version, and activate an older version to make it live — without shipping a new app release.
Viewing history
Click History on any prompt card to open the version history panel. Each entry shows the version number and the date it was saved.
Previewing a version
Click Preview next to any version to view its messages in read-only mode. No changes are made until you explicitly activate a version.
Activating a version
Click Activate (from the history list or from within the preview) and confirm the prompt. Activating creates a new version with the content of the selected snapshot — it does not rewrite history. For example, if the current version is v3 and you activate v1, the prompt becomes v4 with v1's content. You can then edit it further to create v5.
After activation, client apps receive the updated content on their next sync() call, exactly as they would after a normal save.
Version retention by plan
| Plan | Version history |
|---|---|
| Free | Not available |
| Starter | Last 5 versions |
| Pro | Last 10 versions |
| Business | Last 15 versions |
Once the retention limit is reached, the oldest snapshot is automatically removed when a new one is recorded. The active version of a prompt is never affected — only history snapshots are pruned.
SDK version pinning
When a prompt receives a major bump, it means a new {{VARIABLE}} was added. If your app hasn't been updated to supply that variable yet, getPrompt(named:variables:) will return it in missingVariables and leave the placeholder unreplaced — which is visible to end users.
Version pinning solves this. Pin a prompt to a major version number and the SDK will only ever receive minor updates within that major, no matter how far ahead the server version moves. A major bump is delivered only when you explicitly bump the pin and ship an app update that handles the new variable.
Configuration
There are two ways to configure pinning — they can be combined (runtime calls take precedence over the file).
JSON config file (recommended for most apps)
Create a file named maig-prompts.json and add it to your app's bundle (Xcode) or src/main/assets/ folder (Android). The SDK looks for this file by default — no extra configuration needed.
{
"pinned": {
"support-bot": 1,
"onboarding-flow": 2
}
}
The key is the prompt name; the value is the major version to pin to. This file is version-controlled alongside your code, making it easy to review pin changes in pull requests.
iOS
// No configFile argument needed — SDK loads maig-prompts.json from the bundle automatically
let store = PromptStore(apiKey: "maig_YOUR_PROJECT_KEY")
// Custom filename: pass the name without the .json extension
let store = PromptStore(apiKey: "maig_YOUR_PROJECT_KEY", configFile: "my-custom-config")
// Disable config file loading entirely
let store = PromptStore(apiKey: "maig_YOUR_PROJECT_KEY", configFile: nil)
// Pin at runtime (overrides the file)
store.pin("support-bot", majorVersion: 1)
try await store.sync()
Android
// No configFile argument needed — SDK loads maig-prompts.json from assets automatically
val store = PromptStore(apiKey = "maig_YOUR_PROJECT_KEY", context = applicationContext)
// Custom filename
val store = PromptStore(
apiKey = "maig_YOUR_PROJECT_KEY",
context = applicationContext,
configFile = "my-custom-config.json",
)
// Disable config file loading entirely
val store = PromptStore(
apiKey = "maig_YOUR_PROJECT_KEY",
context = applicationContext,
configFile = null,
)
// Pin at runtime (overrides the file)
store.pin("support-bot", majorVersion = 1)
store.sync()
Pinning behaviour
| Scenario | Server returns |
|---|---|
| Pinned to v1, client has v1.2 cached, server has v1.3 | v1.3 content |
| Pinned to v1, client has v1.2 cached, server is on v2.0 only | Nothing — client keeps v1.2 |
| Pinned to v1, client has no cache, server has v1.x available | Latest v1.x (client gets a usable prompt on first launch) |
| Not pinned, hash mismatch | Latest version regardless of major |
| Not pinned, hash matches | Nothing |
Upgrading to a new major
When you are ready to adopt v2.0 of a prompt:
- Update your app code to supply the new variable.
- Bump the pin from
1to2in the config file or runtime call. - Ship the app update — on the next
sync(), the SDK will receive v2.x content.
Variable substitution
Embed {{VARIABLE_NAME}} placeholders anywhere in a message's content field. At inference time, pass a variables map to getPrompt and the SDK returns new Message objects with the placeholders replaced — the cached template is never modified.
Variable names must start with a letter or underscore and contain only letters, digits, and underscores (e.g. USER_NAME, productTitle). Names are case-sensitive. If a placeholder key is missing from the supplied map, it is left verbatim in the content so the mismatch is immediately visible.
iOS
// Store the template in the dashboard:
// "You are a support agent for {{PRODUCT}}. Greet {{USER}} warmly."
if let result = store.getPrompt(named: "support-greeting", variables: [
"PRODUCT": "Acme Store",
"USER": username,
]) {
// result.messages — substituted, ready to pass to generateText
// result.missingVariables — placeholders that had no supplied value
// result.extraVariables — supplied keys that didn't match any placeholder
if !result.missingVariables.isEmpty {
print("Missing variables:", result.missingVariables)
}
let messages = result.messages + [Message.user("I need help with my order.")]
let response = try await client.generateText(messages: messages)
}
The no-variables getPrompt(named:) overload is unchanged and returns [Message]? directly.
Android
val result = store.getPrompt("support-greeting", mapOf(
"PRODUCT" to "Acme Store",
"USER" to username,
))
if (result != null) {
// result.messages — substituted messages
// result.missingVariables — placeholders with no supplied value
// result.extraVariables — supplied keys that matched no placeholder
if (result.missingVariables.isNotEmpty()) {
Log.w("PromptStore", "Missing variables: ${result.missingVariables}")
}
val messages = result.messages + Message(role = "user", content = "I need help.")
val response = client.generateText(messages = messages)
}
The single-argument getPrompt(name) overload is unchanged and returns List<Message>? directly.
Sync endpoint reference
The SDK calls this endpoint internally. You do not need to call it directly.
POST https://api.maig.dev/v1/prompts/sync
Headers: Authorization: Bearer <project-api-key>, Content-Type: application/json
Body: JSON object with two optional fields:
hashes— a map of{ [name]: contentHash }representing what the client currently has cached. Omit or send an empty object on first sync to receive all prompts.pinned— a map of{ [name]: majorVersion }for any prompts the client has pinned to a major version. Omit for prompts that should always receive the latest.
{
"hashes": { "support-bot-system": "a3f8...", "onboarding-flow": "d4e5..." },
"pinned": { "support-bot-system": 1 }
}
Response:
{
"prompts": [
{
"name": "support-bot-system",
"majorVersion": 1,
"minorVersion": 3,
"contentHash": "a3f8...",
"messages": [
{ "role": "system", "content": "You are a helpful support agent." }
]
}
],
"deletedNames": ["old-prompt"]
}
Only prompts whose contentHash differs from the client's cached hash are included in prompts. For pinned prompts, the server only returns versions within the pinned major — if no update is available within that major, the prompt is omitted entirely. deletedNames lists any names the client sent in hashes that no longer exist on the server.
Next steps
- See the iOS SDK or Android SDK guide for full installation and usage instructions
- Questions? Email support@maig.dev