12-factor-agents's full system prompt: 3 versions, 14,457 characters. Audited against AISPA.
The full text of 3
prompts is reproduced below,
14,457 characters in all, each read
instruction by instruction against the eight
AISPA dimensions.
Nothing was flagged as working against the person on
the other end.
# Chapter 1 - CLI and Agent Loop
Now let's add BAML and create our first agent with a CLI interface.
First, we'll need to install [BAML](https://github.com/boundaryml/baml)
which is a tool for prompting and structured outputs.
npm install @boundaryml/baml
Initialize BAML
npx baml-cli init
Remove default resume.baml
rm baml_src/resume.baml
Add our starter agent, a single baml prompt that we'll build on
cp ./walkthrough/01-agent.baml baml_src/agent.baml
<details>
<summary>show file</summary>
```rust
// ./walkthrough/01-agent.baml
class DoneForNow {
intent "done_for_now"
message string
}
function DetermineNextStep(
thread: string
) -> DoneForNow {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a helpful assistant that can help with tasks.
{{ _.role("user") }}
You are working on the following thread:
{{ thread }}
What should the next step be?
{{ ctx.output_format }}
"#
}
test HelloWorld {
functions [DetermineNextStep]
args {
thread #"
{
"type": "user_input",
"data": "hello!"
}
"#
}
}
```
</details>
Generate BAML client code
npx baml-cli generate
Enable BAML logging for this section
export BAML_LOG=debug
Add the CLI interface
cp ./walkthrough/01-cli.ts src/cli.ts
<details>
<summary>show file</summary>
```ts
// ./walkthrough/01-cli.ts
// cli.ts lets you invoke the agent loop from the command line
import { agentLoop, Thread, Event } from "./agent";
export async function cli() {
// Get command line arguments, skipping the first two (node and script name)
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Error: Please provide a message as a command line argument");
process.exit(1);
}
// Join all arguments into a single message
const message = args.join(" ");
// Create a new thread with the user's message as the initial event
const thread = new Thread([{ type: "user_input", data: message }]);
// Run the agent loop with the thread
const result = await agentLoop(thread);
console.log(result);
}
```
</details>
Update index.ts to use the CLI
```diff
src/index.ts
+import { cli } from "./cli"
+
async function hello(): Promise<void> {
console.log('hello, world!')
async function main() {
- await hello()
+ await cli()
}
```
<details>
<summary>skip this step</summary>
cp ./walkthrough/01-index.ts src/index.ts
</details>
Add the agent implementation
cp ./walkthrough/01-agent.ts src/agent.ts
<details>
<summary>show file</summary>
```ts
// ./walkthrough/01-agent.ts
import { b } from "../baml_client";
// tool call or a respond to human tool
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
export interface Event {
type: string
data: any;
}
export class Thread {
events: Event[] = [];
constructor(events: Event[]) {
this.events = events;
}
serializeForLLM() {
// can change this to whatever custom serialization you want to do, XML, etc
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
return JSON.stringify(this.events);
}
}
// right now this just runs one turn with the LLM, but
// we'll update this function to handle all the agent logic
export async function agentLoop(thread: Thread): Promise<AgentResponse> {
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
return nextStep;
}
```
</details>
The the BAML code is configured to use OPENAI_API_KEY by default
As you're testing, you can change the model / provider to something else
as you please
client "openai/gpt-4o"
[Docs on baml clients can be found here](https://docs.boundaryml.com/guide/baml-basics/switching-llms)
For example, you can configure [gemini](https://docs.boundaryml.com/ref/llm-client-providers/google-ai-gemini)
or [anthropic](https://docs.boundaryml.com/ref/llm-client-providers/anthropic) as your model provider.
If you want to run the example with no changes, you can set the OPENAI_API_KEY env var to any valid openai key.
export OPENAI_API_KEY=...
Try it out
npx tsx src/index.ts hello
you should see a familiar response from the model
{
intent: 'done_for_now',
message: 'Hello! How can I assist you today?'
}
# Chapter 1 - CLI and Agent Loop
Now let's add BAML and create our first agent with a CLI interface.
First, we'll need to install [BAML](https://github.com/boundaryml/baml)
which is a tool for prompting and structured outputs.
npm install @boundaryml/baml
Initialize BAML
npx baml-cli init
Remove default resume.baml
rm baml_src/resume.baml
Add our starter agent, a single baml prompt that we'll build on
cp ./walkthrough/01-agent.baml baml_src/agent.baml
<details>
<summary>show file</summary>
```rust
// ./walkthrough/01-agent.baml
class DoneForNow {
intent "done_for_now"
message string
}
client<llm> Qwen3 {
provider "openai-generic"
options {
base_url env.BASETEN_BASE_URL
api_key env.BASETEN_API_KEY
}
}
function DetermineNextStep(
thread: string
) -> DoneForNow {
client Qwen3
// use /nothink for now because the thinking tokens (or streaming thereof) screw with baml (i think (no pun intended))
prompt #"
{{ _.role("system") }}
/nothink
You are a helpful assistant that can help with tasks.
{{ _.role("user") }}
You are working on the following thread:
{{ thread }}
What should the next step be?
{{ ctx.output_format }}
"#
}
test HelloWorld {
functions [DetermineNextStep]
args {
thread #"
{
"type": "user_input",
"data": "hello!"
}
"#
}
}
```
</details>
Generate BAML client code
npx baml-cli generate
Enable BAML logging for this section
export BAML_LOG=debug
Add the CLI interface
cp ./walkthrough/01-cli.ts src/cli.ts
<details>
<summary>show file</summary>
```ts
// ./walkthrough/01-cli.ts
// cli.ts lets you invoke the agent loop from the command line
import { agentLoop, Thread, Event } from "./agent";
export async function cli() {
// Get command line arguments, skipping the first two (node and script name)
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Error: Please provide a message as a command line argument");
process.exit(1);
}
// Join all arguments into a single message
const message = args.join(" ");
// Create a new thread with the user's message as the initial event
const thread = new Thread([{ type: "user_input", data: message }]);
// Run the agent loop with the thread
const result = await agentLoop(thread);
console.log(result);
}
```
</details>
Update index.ts to use the CLI
```diff
src/index.ts
+import { cli } from "./cli"
+
async function hello(): Promise<void> {
console.log('hello, world!')
async function main() {
- await hello()
+ await cli()
}
```
<details>
<summary>skip this step</summary>
cp ./walkthrough/01-index.ts src/index.ts
</details>
Add the agent implementation
cp ./walkthrough/01-agent.ts src/agent.ts
<details>
<summary>show file</summary>
```ts
// ./walkthrough/01-agent.ts
import { b } from "../baml_client";
// tool call or a respond to human tool
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
export interface Event {
type: string
data: any;
}
export class Thread {
events: Event[] = [];
constructor(events: Event[]) {
this.events = events;
}
serializeForLLM() {
// can change this to whatever custom serialization you want to do, XML, etc
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
return JSON.stringify(this.events);
}
}
// right now this just runs one turn with the LLM, but
// we'll update this function to handle all the agent logic
export async function agentLoop(thread: Thread): Promise<AgentResponse> {
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
return nextStep;
}
```
</details>
The the BAML code is configured to use BASETEN_API_KEY by default
To get a Baseten API key and URL, create an account at [baseten.co](https://baseten.co),
and then deploy [Qwen3 32B from the model library](https://www.baseten.co/library/qwen-3-32b/).
```rust
function DetermineNextStep(thread: string) -> DoneForNow {
client Qwen3
// ...
```
If you want to run the example with no changes, you can set the BASETEN_API_KEY env var to any valid baseten key.
If you want to try swapping out the model, you can change the `client` line.
[Docs on baml clients can be found here](https://docs.boundaryml.com/guide/baml-basics/switching-llms)
For example, you can configure [gemini](https://docs.boundaryml.com/ref/llm-client-providers/google-ai-gemini)
or [anthropic](https://docs.boundaryml.com/ref/llm-client-providers/anthropic) as your model provider.
For example, to use openai with an OPENAI_API_KEY, you can do:
client "openai/gpt-4o"
Set your env vars
export BASETEN_API_KEY=...
export BASETEN_BASE_URL=...
Try it out
npx tsx src/index.ts hello
you should see a familiar response from the model
{
intent: 'done_for_now',
message: 'Hello! How can I assist you today?'
}
Walkthroughgen is a tool for creating walkthroughs, tutorials, readmes, and documentation.
## Usage
You create a walkthrough by writing a simple yaml file that describes the walkthrough. In the file, you reference the incremental files that should exist at each step of the walkthrough
```
├── walkthrough
│ ├── 00-package-lock.json
│ ├── 00-package.json
│ ├── 01-index.ts
│ ├── 02-cli.ts
│ └── 02-index.ts
└── walkthrough.yaml
```
Your walkthrough.yaml file might look like this (runnable example in [examples/typescript-cli](./examples/typescript))
```yaml
title: "setting up a typescript cli"
text: "this is a walkthrough for setting up a typescript cli"
targets:
- markdown: "./build/walkthrough.md" # generates a walkthrough.md file
onChange: # default behavior - on changes, show diffs and cp commands
diff: true
cp: true
newFiles: # when new files are created, just show the copy command
cat: false
cp: true
- final: "./build/final" # outputs the final project to the final folder
- folders: "./build/by-section" # creates a separate working folder for each section
sections:
- name: setup
title: "Copy initial files"
steps:
- file: {src: ./walkthrough/00-package.json, dest: package.json}
- file: {src: ./walkthrough/00-package-lock.json, dest: package-lock.json}
- file: {src: ./walkthrough/00-tsconfig.json, dest: tsconfig.json}
- name: initialize
title: "Initialize the project"
steps:
- text: "initialize the project"
command: |
npm install
- text: "then add index.ts"
file: {src: ./walkthrough/01-index.ts, dest: src/index.ts}
- text: "run it with tsx"
command: |
npx tsx src/index.ts
results:
- text: "you should see a hello world message"
code: |
hello world
- name: add-cli
title: "Add a CLI"
steps:
- text: "add a cli"
file: {src: ./walkthrough/02-cli.ts, dest: src/cli.ts}
- text: "add a cli"
file: {src: ./walkthrough/02-index.ts, dest: src/index.ts}
```
Build the project with:
```
npm i -g wtg
wtg build
```
based on your targets, this would create the following files
```
├── walkthrough
│ ├── 00-package-lock.json
│ ├── 00-package.json
│ ├── 01-index.ts
│ ├── 02-cli.ts
│ └── 02-index.ts
├── build
│ ├── by-section
│ │ ├── 00-initialize # only contains the files in `init`
│ │ │ ├── readme.md # contains steps for this section
│ │ │ ├── package.json
│ │ │ ├── package-lock.json
│ │ │ └── tsconfig.json
│ │ └── 01-add-cli # contains the files up to the START of section 1
│ │ ├── readme.md # contains steps for this section
│ │ ├── package.json
│ │ ├── package-lock.json
│ │ ├── tsconfig.json
│ │ └── src
│ │ └── index.ts
│ ├── final
│ │ ├── package.json
│ │ ├── package-lock.json
│ │ ├── tsconfig.json
│ │ └── src
│ │ ├── cli.ts
│ │ └── index.ts
│ └── walkthrough.md
and your walkthrough.md file will look like:
```markdown
# Setting up a typescript cli
this is a walkthrough for setting up a typescript cli
## Copy initial files
cp walkthrough/00-package.json package.json
cp walkthrough/00-package-lock.json package-lock.json
cp walkthrough/00-tsconfig.json tsconfig.json
## Initialize the project
initialize the project
npm install
then add index.ts
cp walkthrough/01-index.ts src/index.ts
and run it with tsx
npx tsx src/index.ts
you should see a hello world message
hello world
## Add a CLI
add a cli
```
```
cp walkthrough/02-cli.ts src/cli.ts
update index.ts to use the cli
```diff
const main = async () => {
+ return cli();
};
main();
```
or just:
cp walkthrough/02-index.ts src/index.ts
```
## Features
### Targets
- `file`: generates a single markdown file
- `folder`: creates a set of folders, one for each section
- `final`: outputs the final project to the current directory
### Init
### Sections
### Steps
#### Step
## Walkthrough.yaml for walkthroughgen
## Implementation Plan
- [ ] implement core walkthroughgen CLI - `wtg build` # defaults to walkthrough.yaml in current directory
- Scope 1: generating walkthrough.md
- [ ] create end-to-end test for a simple walkthrough file, just a single yaml file with no sections
- [ ] create end-to-end test for a walkthrough file with a single section
- [ ] test generation of diffs and cp commands
- Scope 2: generating final/ project build
- [ ] create end-to-end test for a walkthrough file with a final target
- Scope 3: generating by-section project builds with readmes
- [ ] create end-to-end test for a walkthrough file with a by-section target
Questions about 12-factor-agents's system prompt
Does 12-factor-agents's system prompt contain instructions that work against the user?
No. Nothing in 12-factor-agents's system prompt was flagged as working against the person the product is talking to. That is a clean result across all eight AISPA dimensions, not an absence of checking — the full text was read instruction by instruction.
How long is 12-factor-agents's system prompt?
14,457 characters across 3 prompts on this page. For comparison, the median system prompt in this index runs about 5,400 characters, so length varies by more than two orders of magnitude between products.
How many versions of 12-factor-agents's system prompt are on record?
3. Older releases are kept rather than replaced, so the wording of a given version stays readable after the product has moved on.
Where did this 12-factor-agents system prompt come from?
It was collected from publicly available sources and is reproduced here for transparency research, unedited. This site does not extract prompts from products itself.
How was 12-factor-agents's system prompt audited?
Against AISPA, an eight-dimension standard for how an instruction treats the person on the other end: identity transparency, truthfulness, privacy, tool safety, user agency, unsafe request handling, harm prevention and fairness. This audit was ai audit. The method is described in the paper behind the standard.
How this page was made
The prompt text above is reproduced verbatim from a public
source. Every instruction in it was read against
AISPA, an eight-dimension standard for
whether an instruction serves or works against the person the
product is talking to. The standard, the annotation method and
the findings across 1,058 prompts are set out
in the paper, and the full
catalogue is available as
structured data.
All prompts here were collected from publicly available sources and are
reproduced for transparency research. Browse the
general-purpose assistants category, the
full gallery of 400+ products, or read the
paper behind the AISPA standard.