# AGENTS
Source: https://dev.writer.com/AGENTS



# Developer documentation style guide

This guide covers how to write and organize documentation for Writer's developer platform. It combines content strategy with style rules to create docs that get developers productive quickly.

> \[!NOTE]
> This guide is for developer documentation. Writer maintains a [general style guide](https://writer.styleguide.com/) for other marketing and product writing.

## Foundation

* Follow [Google's developer documentation style guide](https://developers.google.com/style) as the base reference
* When in doubt, defer to Google's guidelines unless explicitly overridden below
* Check Vale rules and dictionaries for approved terminology and product names

## Content principles

### Core philosophy

Documentation should get developers productive as quickly as possible. Every piece of content should either help them complete a task or understand why that task matters.

Developers come to documentation to solve specific problems, like implementing a feature or debugging an issue. They scan for relevant information, jump between sections, and search for targeted solutions. They don't read docs cover-to-cover.

Documentation should be organized to support this problem-solving behavior with clear headings, easy navigation, and solutions that are quick to find.

### Content strategy

#### Start with outcomes, not features

* Lead every document with what the developer will accomplish
* Explain the business value or technical benefit upfront

#### Show, don't just tell

* Lead with working code examples, not conceptual explanations
* Use realistic data and scenarios in examples
* Demonstrate the "happy path" first, then cover edge cases

#### Progressive complexity

* Start with the smallest possible working example
* Build complexity gradually in logical steps
* Each section should build on knowledge from previous sections
* Layer on configuration options and advanced features incrementally

#### Focus on success paths

* Document the most common, successful implementation patterns
* Show working examples that developers can build on
* Provide clear verification steps so developers know they're on track
* Keep examples focused on core functionality rather than edge cases

### Content organization

#### Task-oriented structure

* Organize content around what developers need to do, not product features
* Use action-oriented headings that describe outcomes ("Chat with an LLM" not "Chat completions")
* Use descriptive headings that match how people search ("Fix authentication errors" not "Error handling")
* Group related tasks together logically
* Provide clear pathways between related tasks

#### Front-load solutions

* Put the most common answer or code example at the top of each section
* Lead with "here's how to do X" before explaining background theory
* Save conceptual explanations for later sections
* Each heading should solve one specific problem

#### Scannable structure

* Keep sections short and focused on single problems
* Use visual hierarchy with code blocks, bullet points, and bold text
* Break up walls of text to support scanning behavior
* Create quick reference sections for parameters, error codes, and common patterns

#### Prose after headings (required)

**Always include one or two prose sentences immediately after a heading before any structured content** (bulleted lists, code blocks, tables, etc.). Never jump directly from a heading to a list or code sample. Orient the reader to what the section contains and provide context.

❌ Bad (no prose after heading):

```markdown theme={null}
## Best practices
- Do this
- Do that
```

✅ Good (prose after heading):

```markdown theme={null}
## Best practices
Best practices include proper error handling, secure authentication, and efficient rate limiting. Follow these guidelines to ensure optimal performance and security.
- Do this
- Do that
```

#### Multiple entry points

* Optimize headings for search discoverability
* Cross-link between related content when someone might land on the wrong page
* Provide multiple ways to find the same information
* Structure content to support both search and browsing

#### Content reuse and consistency

* Use snippets for content that appears on multiple pages
* Keep shared information in reusable components
* Maintain consistency in explanations across different contexts
* Update snippets in one place to propagate changes everywhere

#### Minimize context switching

* Keep related information on the same page when possible
* Avoid forcing developers to jump between concept docs and API references
* Include inline parameter descriptions next to code examples
* Provide all necessary information to complete a task in one location

#### Avoid time-sensitive language

* Limit time references to changelog entries and deprecation warnings
  * Don't use "the new feature," "recently," "as of \[date]," or "latest version", which creates documentation debt and makes content feel outdated
  * Replace "As of November 2025, this feature is available..." with "This feature is available..." or "This feature provides..."
  * Replace "The new API endpoint..." with "The API endpoint..." or "This endpoint..."
  * Replace "Recently added support for..." with "Support for..." or "This includes support for..."

## Style rules

### Document structure requirements

#### Opening pattern (required)

Every documentation file must start with:

* What the reader will learn (specific outcomes)
* Why this matters (problem solved or benefit provided)

**Example format:**

```markdown theme={null}
This guide shows you how to [specific action]. After completing these steps, you can [concrete outcome].
```

#### Closing pattern (required)

Every documentation file must end with "Next steps" section containing:

* Links to logical follow-up tutorials
* Related features to explore
* Advanced configuration options, if applicable

### Voice and language rules

#### Always use

* Active voice ("Configure the API" not "The API should be configured")
* Direct address with "you" and "your"
* Present tense for current features ("Add the code" not "You will add the code")
* Sentence case for all headings
* Direct, imperative instructions ("Install the SDK" not "You need to install the SDK")

#### Never use

* First person plural: "we," "us," "our"
  * Creates confusion about whether "we" means Writer or you and the reader
* "Please"
  * Overly polite for technical documentation
* Future tense with "will" for current features
  * Use present tense instead, which feels more immediate and actionable
* Subjective qualifiers: "simple," "basic," "easy," "trivial," "straightforward," "just," "simply," "obviously"
  * Makes people feel inadequate if they struggle with the task
* Marketing language: "powerful," "amazing," "revolutionary," "cutting-edge," "industry-leading," "seamlessly," "effortlessly," "instantly"
  * Sounds promotional rather than helpful
* Banned corporate buzzwords: "leverage," "utilize," "optimize," "robust," "streamline," "synergy," "paradigm," "game-changer," "low-hanging fruit"
  * Vague and overused
* Hyperbolic terms or marketing speak

#### Word replacements:

* "This simple API" → "This API"
* "You need to install X" → "Install X"
* "You need to configure Y" → "Configure Y"
* "Our powerful system" → "The system"
* "Just add the code" → "Add the code"
* "We recommend" → "Use" or "Consider using"
* "This tutorial will show you" → "This tutorial demonstrates"
* "You will be able to" → "You can"
* "Please install" → "Install"
* "Please configure" → "Configure"
* "Utilize" → "Use"
* "Leverage the API" → "Use the API"
* "Click the button" → "Select the button"
* "Since you need to..." → "Because you need to..."

### Content guidelines

#### Code examples:

* **Always provide complete, independently executable examples** - code must be runnable as-is without requiring additional setup
* Always provide complete, working examples with realistic data (not foo/bar placeholders)
* Lead with working code examples, not theory or lengthy explanations
* Include necessary imports and dependencies
* Use realistic variable names and values that match actual use cases
* Add comments for non-obvious code
* Test all examples in multiple environments before publishing
* **Use `<CodeGroup>` component for multi-language examples** (cURL, Python, JavaScript)
* Show multiple language examples when relevant (Python, JavaScript, curl)
* Focus on successful implementation patterns and happy paths
* Keep related code and explanations on the same page to prevent context switching
* Use snippets for code that appears on multiple pages to maintain consistency
* **Always include one or two prose sentences after headings and before code samples** to orient the reader - never jump directly from a heading to code

##### SDK initialization pattern (required for API/SDK examples)

For all API and SDK examples (not Agent Builder), always include the complete initialization pattern. Code samples must be independently executable.

**Python initialization:**

```python theme={null}
from writerai import Writer

# Initialize the Writer client. If you don't pass the `api_key` parameter,
# the client looks for the `WRITER_API_KEY` environment variable.
client = Writer()
```

**JavaScript initialization:**

```javascript theme={null}
import { Writer } from "writer-sdk";

// Initialize the Writer client. If you don't pass the `api_key` parameter,
// the client looks for the `WRITER_API_KEY` environment variable.
const client = new Writer();
```

**Note:** Agent Builder examples do not follow this pattern as they use the Agent Builder framework, not the SDK directly.

##### CodeGroup component usage

Always use `<CodeGroup>` for code samples, especially for cURL, Python, and JavaScript examples. This is not necessary for standalone JSON examples or Agent Builder content (which is Python-only), but is required for:

* API endpoint examples (include cURL, Python, and JavaScript)
* SDK usage examples (include Python and JavaScript where applicable)
* Any code that shows implementation across multiple languages

**Example:**

````mdx theme={null}
<CodeGroup>
```bash cURL
curl -X POST https://api.writer.com/v1/chat/completions \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "palmyra-x-004",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'
```

```python Python
from writerai import Writer

# Initialize the Writer client. If you don't pass the `api_key` parameter,
# the client looks for the `WRITER_API_KEY` environment variable.
client = Writer()

response = client.chat.chat(
    model="palmyra-x-004",
    messages=[{"role": "user", "content": "Hello!"}]
)
```

```javascript JavaScript
import { Writer } from "writer-sdk";

// Initialize the Writer client. If you don't pass the `api_key` parameter,
// the client looks for the `WRITER_API_KEY` environment variable.
const client = new Writer();

const response = await client.chat.chat({
  model: "palmyra-x-004",
  messages: [{ role: "user", content: "Hello!" }]
});
```
</CodeGroup>
````

##### Mintlify component imports

**Important:** Built-in Mintlify components like `<CodeGroup>`, `<Steps>`, `<Card>`, `<Note>`, `<Warning>`, `<Tip>`, etc. do NOT need to be imported. They are available by default in all MDX files.

**Only snippets need to be imported.** For example:

```mdx theme={null}
import feedback from '/snippets/agent-builder-feedback.mdx';
```

#### Alt text guidelines for images

* Alt text provides a concise description that replaces the image when it's not visible
* Alt text should consider the context of the image, not just its content
* The goal: replacing every image with its alt text should not change the page's meaning

##### When to use alt text

* **Informative images**: Images that convey information not present in surrounding text
* **Functional images**: Images that serve as interactive elements (buttons, links)
* **Complex images**: Charts, diagrams, or screenshots that need description

##### When to use empty alt text

You can leave the alt text empty `![](/images/image.png)` in the following cases, so screen readers skip the image:

* **Decorative images**: Images that exist only for visual appeal
* **Redundant images**: Images whose information is already expressed in surrounding text
* **UI screenshots**: Screenshots that exist just for visual reference
* **UI icons**: Interface icons that are decorative or have text labels
* **Visual aids**: Images that support information already present in text

##### Alt text writing guidelines

* **Length**: Keep alt text under 155 characters
* **Content**: Don't include "Image of" or "Photo of" phrases
* **Punctuation**: Include punctuation for natural speech pauses
* **Format**: Use full sentences or noun phrases
* **Capitalization**: Avoid all-caps (screen readers may read each letter individually)
* **Context**: Consider the image's purpose in the page, not just visual content
* **Consistency**: Use consistent alt text for repeated images (controls, status indicators, icons)

##### Examples of good alt text

* `![Architecture of an app that's built with Apps Script](/images/architecture.png)`
* `![A card message](/images/card-message.png)`
* `![User interface showing the login form with username and password fields](/images/login-form.png)`
* `![Flowchart showing the authentication process from login to dashboard](/images/flowchart.png)`

##### Examples of when to use empty alt text

* `![](/images/icon.png)` for decorative icons that have text labels
* `![](/images/screenshot.png)` for UI screenshots that are explained in surrounding text
* `![](/images/divider.png)` for visual dividers or decorative elements

#### API documentation:

* Start with what the endpoint does
* List required parameters before optional ones
* Include realistic request/response examples
* Document error conditions and responses

#### Tutorials:

* Use numbered steps for procedures
* Include verification steps ("You should see...")
* Provide troubleshooting for common issues
* End with clear next steps

### Formatting rules

#### Numbers and punctuation:

* Always use Oxford comma in lists
* Spell out numbers 0-9, use numerals for 10+ unless it would be mixing numerals and words (ex: "there are 2 people and 15 dogs" is correct so that the numerals are not mixed with words)
* Spell out numbers that start sentences
* No periods in acronyms (UK not U.K.)
* Use "generative AI" not "GenAI" or "gen AI"

#### Headings:

* Use sentence case for all headings: "Configure authentication" not "Configure Authentication"
* Only capitalize the first word and proper nouns
* Use action-oriented headings with imperative verbs that describe what the user will do
* Be specific and descriptive about what the user will accomplish

**Action-oriented heading patterns:**

Always start headings with imperative verbs. Headings should tell users what they will do, not describe what exists.

**Common transformations:**

* ❌ "Key differences" → ✅ "Compare the APIs"
* ❌ "Parameter mapping" → ✅ "Map your parameters"
* ❌ "Accessing data" → ✅ "Access your data"
* ❌ "Migration steps" → ✅ "Migrate your code"
* ❌ "Next steps" → ✅ "Explore related features"
* ❌ "Prerequisites" → ✅ "Prepare your environment"
* ❌ "Configuration" → ✅ "Configure the settings"
* ❌ "Installation" → ✅ "Install the SDK"
* ❌ "Overview" → ✅ "Understand the basics" or "Learn about \[feature]"
* ❌ "Authentication" → ✅ "Authenticate your requests"
* ❌ "Error handling" → ✅ "Handle errors"
* ❌ "Best practices" → ✅ "Follow best practices" or "Optimize your implementation"

**Examples of correct sentence case with action verbs:**

* ✅ "Set up your development environment"
* ✅ "Send your first message"
* ✅ "Stream responses in real-time"
* ✅ "Configure authentication"
* ✅ "Access translation metadata"
* ✅ "Map your parameters"
* ❌ "Set Up Your Development Environment"
* ❌ "Chat Completions"
* ❌ "API Reference"
* ❌ "Key differences"
* ❌ "Prerequisites"

**Avoid these patterns:**

* Gerunds as headings: "Configuring", "Installing", "Setting up"
* Noun phrases: "Configuration", "Installation", "Overview", "Introduction"
* Questions: "How do I configure X?" (just use the imperative: "Configure X")
* Passive constructions: "Getting started"

#### Links:

* Use descriptive link text, never "click here", "read more", or "this link". "Learn more about authentication methods" is good, but "click here for documentation" is not.
* Provide context for external links
* Ensure all links are current and accessible
* Run `mintlify broken-links` to check for broken links before publishing

## Example quality standards

### Realistic and tested examples

* Use actual product data and realistic scenarios
* Test all code examples in multiple environments
* Show examples in multiple programming languages (Python, JavaScript) if available

### Complete and runnable code

* Provide full working examples, not snippets that require guessing
* Include all necessary imports, dependencies, and setup steps
* Show the complete request/response cycle
* Include error handling in examples when appropriate

### Contextual examples

* Match examples to the specific use case being documented
* Use variable names and data that reflect real-world usage
* Show examples that developers can easily adapt to their own needs
* Include multiple examples when there are common variations

## Quality checklist

Before submitting documentation, verify:

* [ ] Opens with value statement (what, why, expectations)
* [ ] Uses sentence case for all headings
* [ ] No subjective qualifiers or marketing language
* [ ] Active voice and present tense throughout
* [ ] Direct address using "you" (no "we/us/our")
* [ ] Direct imperative instructions (no "you need to")
* [ ] Code examples are complete and tested
* [ ] All links work and are current
* [ ] Ends with actionable next steps
* [ ] Terminology matches Vale rules and approved dictionaries

## Examples

### Good opening:

```markdown theme={null}
# Set up webhook notifications

This guide shows you how to configure webhooks to receive real-time updates when users complete actions in your application. After setting up webhooks, your application can respond immediately to events without polling Writer's API.
```

### Good closing:

```markdown theme={null}
## Next steps

Now that you have webhooks configured, you can:
- [Handle webhook failures](link) to ensure reliable event processing
- [Secure your webhook endpoints](link) with signature verification
- [Set up webhook monitoring](link) to track delivery success
```

### Voice transformation:

❌ "Our powerful API makes it easy to integrate authentication. We've designed a simple system that will help you get started quickly."

✅ "The authentication API handles user login and token management. Follow these steps to integrate authentication into your application."

❌ "You need to install the Writer SDK before you can start building."

✅ "Install the Writer SDK before building your application."

***

*This guide is a living document. Submit suggestions and updates through the standard documentation review process.*


# Understand agent architecture
Source: https://dev.writer.com/agent-builder/agent-architecture

Understand how Agent Builder agents work. Learn about UI components, blueprints, state management, and how they connect to create functional agents.

<iframe title="Your Video Title" />

An Agent Builder agent consists of a UI, which is the agent's interface, and a blueprint, which is the agent's logic. The UI and blueprint are connected through the agent's state and UI triggers.

<img alt="Agent blueprint showing the relationship between UI, blueprint, and state" />

## Agent components

An agent consists of the following components:

* **UI**: the interface that users interact with.
  * UIs are designed using elements like input fields, buttons, and embeds, including support for guided workflows with pagination.
  * They give users a structured, intuitive way to engage with agents, from simple chat interfaces to rich, interactive tools.
  * The UI is optional. If you do not need to receive user input or display output, you can build an agent without a UI by only using the blueprint.
* **Blueprint**: a visual map of the agent's business logic and behavior.
  * Blueprints are created using a library of configurable blocks for tool calling, built-in RAG, text generation, classification, state management, and more.
  * They define how the agent processes input, makes decisions, and takes action—enabling it to reliably orchestrate work across people, data, systems, and even other agents built in Writer.

An agent can also include:

* Custom Python code to extend its capabilities with more complex logic.
* [Secrets](/agent-builder/secrets) to store sensitive information like API keys, passwords, and other credentials and use them in your agent.

Check out the [demo agent walkthrough](/agent-builder/demo-agent) to see how these components work together in practice.

### Connecting components

Agents work by passing data between the UI and blueprint. The following variables are available to each block in the blueprint:

* `@{result}`: Access the output from the previous block in your blueprint
* `@{payload}`: Access data from UI interactions that triggered the blueprint, like button clicks or user messages
* `@{state_variable}`: Access values stored in your agent's state
* `@{vault.secret_name}`: Access secrets stored in your agent's secrets

These variables let you chain blocks together and create dynamic workflows. Learn more in [Using Data from Previous Blocks](/agent-builder/execution-environment).

### Key capabilities

Agent Builder enables agentic workflows with automation and intelligence by providing the following capabilities:

* **Control Flow**: Blueprints support control structures including:
  * Conditional branching based on input analysis or state conditions
  * Iterative loops for processing collections of data or retrying operations
  * Decision trees that route execution based on classification results or business rules
* **Memory**: Agents can use state, environment variables, and Knowledge Graphs to store and retrieve information across blocks and across agent sessions.
* **Knowledge Graph Integration**: Writer's Knowledge Graph provides:
  * Contextual information that enhances agent responses and decision making
  * Semantic search and content discovery
  * Data storage and retrieval across agent sessions
* **Tool calling**: Agents can dynamically analyze requests and intelligently orchestrate external tools, supporting advanced reasoning patterns like ReAct for step-by-step problem solving.
* **Multi-agent workflows**: Built-in blocks enable integration with other Writer agents for complex multi-agent workflows.

### Agent state

Agents use the agent's state to communicate information across components. The state is a shared memory for each part of the agent: the UI, blueprint, and custom Python code can all read and write data to the state.

Learn more in [Agent state](/agent-builder/state).

## Blueprint-only agents

If you don't need to receive user input or display output, you can build an agent without a UI by only using the blueprint.

To run the blueprint without a UI Trigger, press the **Run blueprint** button in the top right of the blueprint.

<img alt="" />

<feedback />


# Trigger blueprints via API
Source: https://dev.writer.com/agent-builder/api-trigger

Enable external systems to trigger blueprints via HTTP API calls. Integrate agents with webhooks, schedulers, and third-party services programmatically.

The [**API Trigger**](/blueprints/apitrigger) block enables external systems to trigger your blueprint via HTTP API calls. Use it to integrate your agent with webhooks, external services, or other applications that need to programmatically execute your workflows.

Unlike the [**UI Trigger**](/blueprints/uitrigger) block that responds to user interactions in the interface, the **API Trigger** block accepts HTTP POST requests to trigger blueprint execution.

<Tip>
  You must deploy your agent before you can trigger a blueprint via API.
</Tip>

## Overview

When you add an **API Trigger** block to your blueprint, it creates a public HTTP endpoint that external systems can use to trigger your blueprint execution. This enables:

* **Webhook integration**: Receive notifications from external services like GitHub, Slack, or payment processors
* **API-first workflows**: Build workflows that can be triggered programmatically by other applications
* **Scheduled automation**: Use external schedulers like task schedulers or cloud functions to trigger workflows
* **Cross-service communication**: Enable microservices to trigger agent workflows
* **Third-party integrations**: Allow external tools and platforms to execute your agent's capabilities

## How it works

The API Trigger block creates an HTTP endpoint that external systems can use to trigger your blueprint.

<img alt="API trigger flow showing how external systems send requests to endpoints that trigger blueprint execution" />

Here's what happens when you add an API Trigger block:

### Endpoint creation

The block creates two HTTP endpoints using your agent and blueprint IDs:

* A synchronous endpoint for real-time responses
* An asynchronous endpoint for long-running tasks

### Request handling

When an external system calls your endpoint:

* The request body must contain an `inputs` field with your data
* Your blueprint receives this data as `@{result.inputs}`
* Your blueprint executes with this input data

<Tip>
  #### Accessing the payload data

  `@{result}` is only available in the block that immediately follows the API Trigger block. For subsequent blocks, you have two options to access the payload data:

  1. **Use a Set State block**: Make the first block after API Trigger a Set State block to store the payload data for use throughout your blueprint
  2. **Access via block ID**: Use `@{results.[api_trigger_block_id]}` where `[api_trigger_block_id]` is the unique identifier found in the API Trigger block's configuration menu
</Tip>

### Return values

To send data back to the API caller:

* Add a [**Return Value**](/blueprints/returnvalue) block at the end of your blueprint. The value from this block becomes the API response
* For synchronous calls, the result is the final event in the SSE stream
* For asynchronous calls, the result is available when polling the job status endpoint

### Blueprint structure requirements

When building blueprints that will be triggered via API, ensure your workflow ends with a [**Return Value**](/blueprints/returnvalue) block:

1. **Start with API Trigger**: Begin your blueprint with the API Trigger block
2. **Process your logic**: Add all the blocks needed for your workflow
3. **End with Return Value**: Always finish with a Return Value block to specify what gets returned to the API caller

**Example blueprint structure:**

```
API Trigger → [Your workflow blocks] → Return Value
```

The Return Value block returns any value as a string. If your blueprint returns a JSON object, it will be serialized as a JSON string. You may need to parse it using `JSON.parse()` in JavaScript or `json.loads()` in Python to access the individual fields.

## API endpoint

When you add an **API Trigger** block to your blueprint, it creates a public HTTP endpoint that you can use to trigger your blueprint. You can either use the synchronous endpoint for real-time execution and to get the result as soon as possible, or the asynchronous endpoint for long-running tasks. The asynchronous endpoint returns a job ID that you can poll for status updates and results.

### Authentication

All API calls require authentication using a Bearer token in the Authorization header:

```
Authorization: Bearer <your_api_key>
```

Learn how to [create and manage API keys](/api-reference/api-keys).

### Synchronous endpoint

The endpoint URL follows this pattern:

```
POST https://api.writer.com/v1/agents/{agent_id}/blueprints/{blueprint_id}
```

Where:

* `{agent_id}` is the unique identifier for your deployed agent. You can find this in the URL of your agent's page in the [Agent Builder](https://app.writer.com/agents) or via the [/agents API endpoint](#list-available-agents-and-blueprints).
* `{blueprint_id}` is the unique identifier for your blueprint. You find this in **Blueprint ID** field of the blueprint in the Agent Builder UI, or via the [/agents API endpoint](#list-available-agents-and-blueprints).

The synchronous endpoint is used for real-time execution. It accepts a JSON payload and returns [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events) with real-time execution status.

### Request format

Send a POST request with a JSON body containing the data you want to pass to your blueprint.

<Warning>
  The `inputs` key is required for the JSON payload. If you don't include it, the API will return an error. Include the rest of your data under the `inputs` key. The value of the `inputs` key can be any JSON-serializable value.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.writer.com/v1/agents/123e4567-e89b-12d3-a456-426614174000/blueprints/ooamr04yng7" \
    -H "Authorization: Bearer <your_api_key>" \
    -H "Content-Type: application/json" \
    -d '{"inputs": {"document_id": "doc_67890", "file_url": "https://storage.example.com/documents/invoice_2024_001.pdf", "document_type": "invoice", "priority": "high"}}'
  ```

  ```python Python theme={null}
  import os
  import requests

  # Note about streaming
  # The Python `requests` library with `stream=True` will collect the entire SSE
  # stream and then process it line by line. For true real-time streaming,
  # consider using libraries like `httpx` or `aiohttp` that support async streaming.

  response = requests.post(
      "https://api.writer.com/v1/agents/123e4567-e89b-12d3-a456-426614174000/blueprints/ooamr04yng7",
      headers={"Authorization": f"Bearer {os.environ.get('WRITER_API_KEY')}"},
      json={"inputs": {"document_id": "doc_67890", "file_url": "https://storage.example.com/documents/invoice_2024_001.pdf", "document_type": "invoice", "priority": "high"}},
      stream=True
  )

  # Process Server-Sent Events stream
  for line in response.iter_lines():
      if line:
          line = line.decode('utf-8')
          if line.startswith('data: '):
              data = line[6:]  # Remove 'data: ' prefix
              if data == '[DONE]':
                  break
              print(f"Received: {data}")
  ```

  ```javascript JavaScript theme={null}
  import fetch from 'node-fetch';

  const response = await fetch(
      "https://api.writer.com/v1/agents/123e4567-e89b-12d3-a456-426614174000/blueprints/ooamr04yng7",
      {
          method: 'POST',
          headers: { 
              'Authorization': `Bearer ${process.env.WRITER_API_KEY}`,
              'Content-Type': 'application/json'
          },
          body: JSON.stringify({
              inputs: { document_id: "doc_67890", file_url: "https://storage.example.com/documents/invoice_2024_001.pdf", document_type: "invoice", priority: "high" }
          })
      }
  );

  // Process Server-Sent Events stream
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      const chunk = decoder.decode(value);
      const lines = chunk.split('\n');
      
      for (const line of lines) {
          if (line.startsWith('data: ')) {
              const data = line.slice(6); // Remove 'data: ' prefix
              if (data === '[DONE]') {
                  return;
              }
              console.log('Received:', data);
          }
      }
  }
  ```
</CodeGroup>

This data you send in the request is available in your blueprint under `@{result.inputs}`; for example, `@{result.inputs.document_id}`, `@{result.inputs.file_url}`, etc.

### Response format

The API returns Server-Sent Events (SSE) with real-time execution status:

```
data: {"status":"in progress","message":null}
data: {"status":"initializing","message":"Initializing session..."}
data: {"status":"validating","message":"Validating blueprint..."}
data: {"status":"executing","message":"Executing blueprint: ooamr04yng7..."}
data: {"status":"running","message":"Blueprint is running. Awaiting output..."}
data: {"status":"processing","message":"Processing blueprint result..."}
data: {"delta":"Document processed and routed to accounting team"}
data: [DONE]
```

### Asynchronous execution

For long-running workflows, you can also use the asynchronous endpoint:

```
POST https://api.writer.com/v1/agents/{agent_id}/blueprints/{blueprint_id}/jobs
```

This returns a job ID that you can poll for status updates using:

```
GET https://api.writer.com/v1/agents/jobs/{job_id}
```

#### Example job result

When you poll the job status, you'll receive a response like this:

```json theme={null}
{
  "job_id": "6f6919ba-d97f-4315-bddf-191e5c3efb89",
  "agent_id": "5d746de9-73e7-4c0e-b264-a514f672a3f9",
  "blueprint_id": "ooamr04yng7",
  "status": "completed",
  "artifact": "Document processed and routed to accounting team",
  "error": null,
  "started_at": "2025-08-14T22:26:06.259540Z",
  "finished_at": "2025-08-14T22:26:06.700431Z"
}
```

The actual return value from your agent is in the `artifact` field of the JSON response. The `result` field contains metadata about the job execution.

The `artifact` field always contains a string. If your blueprint returns a JSON object, it will be serialized as a JSON string. You may need to parse it using `JSON.parse()` in JavaScript or `json.loads()` in Python to access the individual fields.

#### Creating and polling async jobs

Here is an example of how to create an async job and then poll for results:

<CodeGroup>
  ```bash cURL theme={null}
  # 1. Create the async job
  curl -X POST "https://api.writer.com/v1/agents/123e4567-e89b-12d3-a456-426614174000/blueprints/ooamr04yng7/jobs" \
    -H "Authorization: Bearer <your_api_key>" \
    -H "Content-Type: application/json" \
    -d '{"inputs": {"document_id": "doc_67890", "file_url": "https://storage.example.com/documents/invoice_2024_001.pdf", "document_type": "invoice", "priority": "high"}}'

  # Response: {"job_id": "job_abc123def456", "status": "queued"}

  # 2. Poll for job status and result
  curl -X GET "https://api.writer.com/v1/agents/jobs/job_abc123def456" \
    -H "Authorization: Bearer <your_api_key>"
  ```

  ```python Python theme={null}
  import os
  import requests
  import time

  def create_and_poll_job(agent_id, blueprint_id, inputs):
      # 1. Create the async job
      url = f"https://api.writer.com/v1/agents/{agent_id}/blueprints/{blueprint_id}/jobs"
      headers = {
          'Authorization': f"Bearer {os.environ.get('WRITER_API_KEY')}",
          'Content-Type': 'application/json'
      }
      
      response = requests.post(url, headers=headers, json={'inputs': inputs})
      job_data = response.json()
      job_id = job_data['job_id']
      
      print(f"Job created: {job_id}")
      
      # 2. Poll for completion
      while True:
          poll_url = f"https://api.writer.com/v1/agents/jobs/{job_id}"
          poll_response = requests.get(poll_url, headers=headers)
          job_status = poll_response.json()
          
          print(f"Job status: {job_status['status']}")
          
          if job_status['status'] in ['completed', 'error']:
              return job_status
          
          time.sleep(2)  # Wait 2 seconds before polling again

  # Example usage
  result = create_and_poll_job(
      '123e4567-e89b-12d3-a456-426614174000',
      'ooamr04yng7',
      {'document_id': 'doc_67890', 'file_url': 'https://storage.example.com/documents/invoice_2024_001.pdf', 'document_type': 'invoice', 'priority': 'high'}
  )
  print(f"Final result: {result}")
  print(f"Artifact: {result['artifact']}")
  ```

  ```javascript JavaScript theme={null}
  import fetch from 'node-fetch';

  async function createAndPollJob(agentId, blueprintId, inputs) {
      // 1. Create the async job
      const createResponse = await fetch(
          `https://api.writer.com/v1/agents/${agentId}/blueprints/${blueprintId}/jobs`,
          {
              method: 'POST',
              headers: {
                  'Authorization': `Bearer ${process.env.WRITER_API_KEY}`,
                  'Content-Type': 'application/json',
              },
              body: JSON.stringify({ inputs }),
          }
      );
      
      const jobData = await createResponse.json();
      const jobId = jobData.job_id;
      
      console.log(`Job created: ${jobId}`);
      
      // 2. Poll for completion
      while (true) {
          const pollResponse = await fetch(
              `https://api.writer.com/v1/agents/jobs/${jobId}`,
              {
                  headers: {
                      'Authorization': `Bearer ${process.env.WRITER_API_KEY}`,
                  },
              }
          );
          
          const jobStatus = await pollResponse.json();
          console.log(`Job status: ${jobStatus.status}`);
          
          if (jobStatus.status === 'completed' || jobStatus.status === 'error') {
              return jobStatus;
          }
          
          // Wait 2 seconds before polling again
          await new Promise(resolve => setTimeout(resolve, 2000));
      }
  }

  // Example usage
  createAndPollJob(
      '123e4567-e89b-12d3-a456-426614174000',
      'ooamr04yng7',
      { document_id: 'doc_67890', file_url: 'https://storage.example.com/documents/invoice_2024_001.pdf', document_type: 'invoice', priority: 'high' }
  ).then(result => {
      console.log('Final result:', result);
      console.log('Artifact:', result.artifact);
  });
  ```
</CodeGroup>

### List available agents and blueprints

To see a list of all your agents and blueprints, you can use the following API endpoint:

```
GET https://api.writer.com/v1/agents?type=builder
```

This returns a list of all your deployed Agent Builder agents and includes any API-triggered blueprints associated with them. You can then use the agent and blueprint IDs to trigger the blueprint via API.

Below is an example of how to list all your agents and blueprints:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.writer.com/v1/agents?type=builder" \
    -H "Authorization: Bearer <your_api_key>"
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.get(
      "https://api.writer.com/v1/agents?type=builder",
      headers={"Authorization": f"Bearer {os.environ.get('WRITER_API_KEY')}"}
  )

  print(response.json())
  ```

  ```javascript JavaScript theme={null}
  import fetch from 'node-fetch';

  const response = await fetch(
      "https://api.writer.com/v1/agents?type=builder",
      { headers: { Authorization: `Bearer ${process.env.WRITER_API_KEY}` } }
  );

  console.log(await response.json());
  ```
</CodeGroup>

## Testing

You can test your API trigger using the **Run blueprint** button in the Agent Builder editor.

Under **Test your trigger** in the API Trigger block, you can provide sample data for testing without making actual HTTP requests.

<img alt="" />

You can also test your API trigger by sending a POST request to the API endpoint.

## Troubleshooting

### Common issues and solutions

#### I can't see or trigger my agent via the API

**Problem**: Your agent doesn't appear in the API or you get errors when trying to trigger it.

**Solution**: Check that your agent is deployed. Only deployed agents are accessible via the API.

#### I don't see my blueprint when looking via the API

**Problem**: Your blueprint doesn't show up in the list of available blueprints for your agent.

**Solution**: Ensure your blueprint starts with an **API Trigger** block. Only blueprints that begin with an API Trigger block are exposed via the API.

#### The blueprint isn't returning anything

**Problem**: Your blueprint executes but doesn't return any data to the API caller.

**Solution**: Make sure you have a [**Return Value**](/blueprints/returnvalue) block at the end of your blueprint workflow. This block is required to return data to the API caller.

#### My blueprint isn't receiving any input values

**Problem**: You can't access the data you sent in your API request.

**Solution**: Remember that your data is nested under the `inputs` key. Use `@{result.inputs.field_name}` instead of `@{result.field_name}`.

### Debugging tips

Since there are limited logs and execution visibility, use the **Return Value** block for debugging:

**Test payload structure**: Create a simple blueprint with just `API Trigger → Return Value` and set the return value to `@{result}`. This will show you exactly what data structure the blueprint is receiving.

**Test specific values**: Use `@{result.inputs}` to see just the inputs portion of your payload.

**Test state variables**: Return `@{state[key]}` to see the value of a specific state variable in your blueprint execution.

**Note**: Since `@{result}` is only available in the block immediately following the API Trigger, consider using a Set State block as your first block to store the payload data for debugging purposes.

## Next steps

* Learn how to [add file inputs and parse PDFs](/agent-builder/summarize-pdfs)
* Explore [tool calling](/agent-builder/tool-calling) for intelligent integrations with external tools
* Build [data tables with DataFrames](/agent-builder/dataframes) to display structured data
* Add [custom Python code](/agent-builder/python-code) to extend your agent's capabilities

<feedback />


# Build a chatbot connected to a Knowledge Graph
Source: https://dev.writer.com/agent-builder/chatbot-tutorial



Agent Builder contains prebuilt UI and blueprint blocks that you can combine to build a full-featured chatbot.

<img alt="" />

This tutorial demonstrates how to build a chatbot that's connected to the Palmyra X5 model and integrates with a Knowledge Graph for domain-specific knowledge. It covers the following steps:

1. Adding a [**Chatbot** block](/components/chatbot) to the UI
2. Adding a [**Chat reply** block](/blueprints/chatreply) to the blueprint
3. Connecting a Knowledge Graph to the chatbot

<Note>
  If you are unfamiliar with Agent Builder interface, check out the [Agent Builder Quickstart](/agent-builder/quickstart).
</Note>

## Build the UI

The UI for the agent contains a single **Chatbot** block. This block handles the full UI for the chatbot, including the chat interface and the chat messages.

<Steps>
  <Step title="Add a Chatbot block to the UI">
    Drag and drop a [**Chatbot** component](/components/chatbot) onto the Interface.

    In the block's configuration panel, set the following fields:

    * **Conversation**: `@{chat}`. This is the state variable that stores the chat conversation.
    * **Assistant initials**: `AI`. This is the text that appears next to the assistant's messages in the chat.
    * **User initials**: `YOU`. This is the text that appears next to the user's messages in the chat.
    * **Use markdown**: `no`. You can change this to `yes` if you want to parse Markdown formatting in messages.
    * **Enable file upload**: `no`. This disables the file uploads in the chatbot.
    * **Placeholder**: `Type your message here.`. This is the text that appears in the input field when it's empty.

      <img alt="" />
  </Step>
</Steps>

## Build the blueprint

The blueprint for the agent contains the following blocks:

* **UI trigger** block to run the blueprint when a user enters a chat message
* **Chat reply** block to manage the conversation and generate a response to the user's message

<Steps>
  <Step title="Add a UI trigger for the chat message event">
    To run the blueprint when a user enters a chat message, add a [**UI trigger** block](/blueprints/uitrigger) to the canvas. In the block's configuration panel, update the following fields:

    * **Component Id**: select the `Chatbot` component from the dropdown of UI blocks
    * **Event type**: `wf-chatbot-message`

      <img alt="" />
  </Step>

  <Step title="Add a Chat reply block">
    Add a **Chat reply** block to the canvas, which runs after the UI trigger block. This block manages the chat conversation, including the chat messages and the chat history.

    Connect the **UI trigger** block to the **Chat reply** block by dragging a line from the green dot on the **UI trigger** block to the **Chat reply** block.

    In the **Chat reply** block's configuration panel, set the following fields:

    * **Conversation object**: `chat`
    * **System prompt**: This provides the context for the chat conversation and gives instructions to the model about how to act or respond. The example below uses a system prompt that instructs the model to act as a helpful assistant regarding Writer's developer platform. System prompts help the model understand the context of the chat conversation and the goals of the chatbot. Learn more in [Prompting strategies](/home/prompting).

    ```
    You are a helpful assistant for Writer's developer platform. Use the provided documentation to answer questions about Writer's APIs, SDKs, and Agent Builder.

    Be clear and concise, include code examples when helpful, and reference specific docs when appropriate. If you're unsure about something, say so and suggest checking the documentation directly.
    ```

    * **Message**: `@{payload}`. This is the message that the user sends to the chatbot that triggers the blueprint to run.

      <img alt="" />
  </Step>
</Steps>

At this point, the chatbot isn't connected to a Knowledge Graph, but it's configured to respond to messages from the user. You can preview the agent to see how it works and then [connect a Knowledge Graph to the chatbot](#connect-a-knowledge-graph-to-the-chatbot) to enable the chatbot to answer domain-specific questions.

## Preview the agent

To preview the agent, navigate to the **Preview** tab. Type a message in the chatbot and see the response from the model.

## Connect a Knowledge Graph to the chatbot

You can connect one or more [Knowledge Graphs](/home/knowledge-graph) to the chatbot to answer questions about data related to your business or a specific dataset.

To connect a Knowledge Graph to the chatbot, update the **Chat reply** block.

<Steps>
  <Step title="Update the Chat reply block">
    * Click the **Chat reply** block to open its configuration panel
    * Click the **+ Add tool** button under **Tools**
    * Select "Knowledge graph" under **Tool type**
    * Under **Graph id(s)**, select one or more Knowledge Graphs you want to connect to the chatbot. The example below connects the chatbot to a Knowledge Graph that contains developer docs and a Knowledge Graph connected to the support center.
    * Click **Save** to save the changes.

      <img alt="" />
  </Step>
</Steps>

Now when the user asks questions related to data in the connected Knowledge Graphs, the chatbot can reference the data in the Knowledge Graphs to answer the question.

If you encounter any issues, refer to the [Troubleshooting](/agent-builder/troubleshooting) guide for debugging information.

<feedback />


# Collaborate on agents
Source: https://dev.writer.com/agent-builder/collaboration



Agent Builder provides the following collaboration tools to help you and your team work together on your agents:

* [Add notes to your agents](#add-notes-to-your-agents)
* [See where users are working in real-time](#see-where-users-are-working-in-real-time)

<img alt="" />

## Add notes to your agents

You can add notes anywhere in an agent's Interface or Blueprint view.

To add a note or view existing notes, click the **Add note** button in the top right corner of the interface or blueprint.

<img alt="" />

Click anywhere in the interface or blueprint to add a note at that location. Type the note in the text box that appears and then click the **Send** button.

<img alt="" />

To view a specific note, click the note icon on the blueprint or interface. This opens the **Note** view to that particular note.

## See where users are working in real-time

If other users are working on the same interface or blueprint, you can see an icon on top of the block they're editing.

<img alt="" />

Changes that other users make to the interface or blueprint automatically apply to your view as well. This allows you to keep your view up to date with the latest changes.

Conflicts can occur if multiple users make changes to the same block at the same time. If you see a user icon on top of a particular block, it means that another user is editing that block and you should wait for them to finish their changes before making any changes to that block yourself.

<feedback />


# Add styling to the agent UI
Source: https://dev.writer.com/agent-builder/component-styles



You can style an agent's interface in two ways:

1. [Using component configuration menus](#component-configuration-menu) to adjust styling such as colors, backgrounds, shadows, and text alignment.

2. [Using CSS](#style-agent-builder-components-with-css) to target specific components, apply more advanced styling not available in the configuration menu, and create reusable styles across multiple components.

The configuration menu provides a convenient interface for common styling needs, while CSS gives you complete control over the agent's appearance.

## Component configuration menu

The component's configuration menu provides an interface for common styling needs such as colors, backgrounds, and text alignment. Each interface component has a different set of configurable attributes, which you can access via the configuration menu.

There are three styling options for each configurable attribute:

* **Default**: the default styling for the attribute.
* **CSS**: provide a CSS rule to apply to the attribute. For example, if the attribute is font color, you can set a CSS value such as `red`, a hex code, or an RGB value to apply to the attribute.
* **Pick**: define a value from a visual picker. For example, you can select a color from a color picker or control a shadow from a set of slider controls.

<Tabs>
  <Tab title="Default">
    Revert to the default styling for the attribute.

    <img alt="" />
  </Tab>

  <Tab title="CSS">
    Provide a CSS rule to apply to the attribute.

    For example, if the attribute is font color, you can set a [CSS value](https://www.w3schools.com/cssref/css_colors.php) such as `red`, a hex code, or an RGB value to apply to the attribute.

    For shadow attributes, you can remove a component's shadow with `none` or update the shadow with [`box-shadow` CSS rules](https://www.w3schools.com/cssref/css3_pr_box-shadow.php).

    <img alt="" />
  </Tab>

  <Tab title="Pick">
    Define a value from a visual picker. For example, you can select a color from a color picker.

    <img alt="" />
  </Tab>
</Tabs>

## Add a CSS stylesheet

To add more specific and detailed styling for your agent's UI, you can add CSS stylesheets.

To style your agent's UI with CSS, follow these steps:

1. Add a CSS file to your project via the **Code** tab
2. Load the stylesheet into the agent's state
3. Apply CSS classes to components via the component's configuration menu

The example below creates a `bubblegum` class to make a pink, rotated component and makes the header font color for `CoreSection h3` elements yellow.

<Steps>
  <Step title="Add the `.css` file to your project">
    Create a new `.css` file in your project or upload an existing one via the **Code** tab.

    This example creates a new file named `theme.css` in the `static` directory with the following content:

    ```css theme={null}
    .bubblegum {
        background: #ff63ca !important;
        line-height: 1.5;
        transform: rotate(-5deg);
    }

    .CoreSection h3 {
        color: #f9ff94 !important;
    }
    ```

    <img alt="" />

    See [Style Agent Builder components with CSS](#style-agent-builder-components-with-css) for more information about specifically styling Agent Builder components.

    <Note>
      To move a file into a directory such as `static`, prepend the directory name to the beginning of the filename. For example, `static/theme.css`.
    </Note>
  </Step>

  <Step title="Import the CSS file in your agent's code">
    To load the stylesheet when the agent starts, import the CSS file using the state's `import_stylesheet` method in `main.py`. The code below imports the stylesheet named `theme` from the `static` directory.

    ```python theme={null}
    initial_state.import_stylesheet("theme", "static/theme.css")
    ```

    <img alt="" />

    The first argument is the name you want to give to the stylesheet, which you can reference if there are multiple stylesheets. The second argument is the path to the CSS file.

    You can also import stylesheets during runtime by calling the `import_stylesheet` method from the `state` object. See [Switch stylesheets during runtime](#switch-stylesheets-during-runtime) for more information.
  </Step>

  <Step title="Apply CSS classes to components">
    In the component's configuration menu, add a CSS class to the component under **Custom CSS classes**. Separate multiple classes with a space.

    This example adds the `bubblegum` class to a `Section` component so that the component's background is pink and rotated 5 degrees. The `CoreSection h3` styling applies to the header for any `Section` components, making the header text yellow.

    <img alt="" />
  </Step>
</Steps>

<Warning>
  **CSS changes may not appear immediately due to browser caching.** When you modify a CSS file and refresh the page, you may experience a delay before the changes apply. This is expected behavior caused by browser caching.

  To see your CSS changes immediately during development, append a version querystring to your stylesheet path. This works because browsers cache CSS files based on their URL. By changing the querystring parameter, you force the browser to treat it as a new file and download a fresh copy. Increment the version number each time you update the CSS file to see changes immediately:

  ```python theme={null}
  initial_state.import_stylesheet("theme", "static/theme.css?v=1")
  # After making changes, update to:
  initial_state.import_stylesheet("theme", "static/theme.css?v=2")
  ```
</Warning>

### Style Agent Builder components with CSS

Agent Builder components each have a class name starting with `Core` that you can use when defining CSS rules.

For example, the `Text` component has the class name `CoreText`. You can use this class name to style all `Text` components with the following CSS rule:

```css theme={null}
.CoreText {
  color: red !important;
}
```

<Tip>
  The `!important` flag is essential when targeting attributes that are configurable via the component's configuration menu, such as text and background colors. Without the `!important` flag, the styles you define in CSS will be overridden by the component's configuration menu styling, because the configuration menu styling is of higher specificity.
</Tip>

### Switch stylesheets during runtime

You can switch stylesheets during runtime by calling the `import_stylesheet` method. To replace the current stylesheet with a new one, use the same stylesheet name but a different path to a new stylesheet.

The example below switches the stylesheet named `theme` to a different file when the `handle_cyberpunk` or `handle_minimalist` functions are called.

```python theme={null}
def handle_cyberpunk(state):
    state.import_stylesheet("theme", "static/cyberpunk_theme.css")

def handle_minimalist(state):
    state.import_stylesheet("theme", "static/minimalist_theme.css")
```

<feedback />


# Build data tables with DataFrames
Source: https://dev.writer.com/agent-builder/dataframes



The [**DataFrame** component](/components/dataframe) lets you display structured data in a table format with built-in features like sorting, searching, and downloading. It's designed to work with [Pandas DataFrames](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) or [PyArrow tables](https://arrow.apache.org/docs/python/generated/pyarrow.Table.html).

## Overview

DataFrames allow you to present data in a grid format. They provide an interface with built-in features like sorting, searching, and downloading. They work well for displaying lists of records, reports, or any tabular data that you've processed with Python.

### When to use DataFrames

DataFrames are useful for:

* **Data analysis results** from pandas operations
* **CSV file uploads** that need to be displayed as tables
* **Database query results** formatted as DataFrames
* **Any data that benefits from sorting and filtering**
* **Reports that users might want to download as CSV**

## Example: Display a sales report

This example creates a sales report from hardcoded data. It processes data with pandas and displays it in a DataFrame.

<Steps>
  <Step title="Open the Code tab and edit the main.py file">
    Open the **Code** tab at the bottom of the agent's interface and navigate to the `main.py` file.

    Add the following code to the `main.py` file. It creates a pandas DataFrame with the sales data and sets it in the state as `sales_report`.

    ```python theme={null}
    import pandas as pd
    import writer as wf

    # Sample sales data
    sales_data = [
        {"rep_name": "Sarah Chen", "region": "West", "product": "Software License", "amount": 15000, "quarter": "Q3 2024"},
        {"rep_name": "Mike Rodriguez", "region": "East", "product": "Consulting", "amount": 8500, "quarter": "Q3 2024"},
        {"rep_name": "Alex Johnson", "region": "Central", "product": "Software License", "amount": 22000, "quarter": "Q3 2024"},
        {"rep_name": "Sarah Chen", "region": "West", "product": "Support Package", "amount": 5000, "quarter": "Q3 2024"},
        {"rep_name": "Lisa Park", "region": "East", "product": "Software License", "amount": 18000, "quarter": "Q3 2024"},
        {"rep_name": "Mike Rodriguez", "region": "East", "product": "Consulting", "amount": 12000, "quarter": "Q3 2024"}
    ]

    # Create pandas DataFrame
    df = pd.DataFrame(sales_data)

    # Set the DataFrame in state
    initial_state = wf.init_state({
     "sales_report": df
    })
    ```
  </Step>

  <Step title="Add a DataFrame component to the agent's interface">
    Navigate to the **Interface** tab to build the agent's interface.

    Add a **DataFrame** component to your page. Update the following settings:

    * **Data**: `@{sales_report}`. This is the state variable that contains the sales report data.

      <img alt="" />
  </Step>
</Steps>

### Preview the DataFrame

Navigate to the **Preview** tab to see the DataFrame in action.

You should see a table with columns for sales rep, region, product, amount, and quarter.

Try clicking column headers to sort by amount or region.

### Enable advanced features

Navigate back to the **Interface** tab to click on the DataFrame component. There, you can enable more features, like search, download, and editing or adding records.

1. **Enable Search**: In the DataFrame component settings, set **Enable search** to `yes`
2. **Enable Download**: Set **Enable download** to `yes`
3. **Optionally enable editing**: Set **Enable adding a record** and **Enable updating a record** to `yes` if you want users to modify data
4. **Test the enhanced features**:
   * Search for specific sales reps or regions
   * Sort by amount to see top performers
   * Download the data as CSV for further analysis

## Example: CSV file upload and processing

This example shows how to upload a CSV file and process the data with pandas to then display it in a DataFrame.

The example assumes you have a CSV file with the following columns:

* `product_category`
* `product_name`
* `quantity`
* `unit_price`
* `sales_rep`
* `date`
* `region`

It processes the data to calculate the total revenue for each product category and displays the data in a DataFrame.

Below is an example of the CSV file that you can use to test the agent:

```csv theme={null}
product_category,product_name,quantity,unit_price,sales_rep,date,region
Software,CRM Pro,2,5000,Sarah Chen,2024-07-15,West
Hardware,Server Rack,1,8000,Mike Rodriguez,2024-07-18,East
Software,Analytics Suite,3,3000,Alex Johnson,2024-07-22,Central
Consulting,Implementation Service,5,1200,Sarah Chen,2024-07-25,West
Hardware,Network Switch,4,2500,Lisa Park,2024-08-02,East
Software,CRM Pro,1,5000,Mike Rodriguez,2024-08-05,East
Consulting,Training Package,8,800,Alex Johnson,2024-08-10,Central
Software,Analytics Suite,2,3000,Sarah Chen,2024-08-12,West
Hardware,Server Rack,2,8000,Lisa Park,2024-08-15,East
Consulting,Implementation Service,3,1200,Mike Rodriguez,2024-08-20,East
Software,CRM Pro,4,5000,Alex Johnson,2024-08-22,Central
Hardware,Network Switch,2,2500,Sarah Chen,2024-08-25,West
```

### Build the interface

Navigate to the **Interface** tab to build the agent's interface.

<Steps>
  <Step title="Add a File input component to allow users to upload the CSV file">
    Add a **File input** component to your page. Update the following settings:

    * **Label**: `Upload CSV`
    * **Allowed file types**: `.csv`
    * **Link variable** under **Binding**: `input_file`
  </Step>

  <Step title="Add a Button component to trigger the file upload">
    Add a **Button** component to your page. Update the following settings:

    * **Label**: `Process CSV`
  </Step>

  <Step title="Add a DataFrame component">
    Add a **DataFrame** component to your page. Update the following settings:

    * **Data**: `@{processed_data}`. This is the state variable that will contain the processed data after you build the blueprint.
  </Step>
</Steps>

The interface should look like this:

<img alt="" />

### Build the blueprint

Open the **Blueprint** tab to build the agent's blueprint. The blueprint contains:

* A **UI Trigger** that triggers blueprint execution when the user clicks the **Upload CSV** button.
* A **Python** block that processes the uploaded file and stores the data in a state variable.

<Steps>
  <Step title="Add a UI Trigger">
    Add a **UI Trigger** to the blueprint. Update the following settings:

    * **Component Id**: Select the **Process CSV** button you added to the interface.
    * **Event type**: `wf-click`
  </Step>

  <Step title="Add a Python code block">
    Add a **Python code** block to the blueprint. Then paste the following code into the block. The code reads the uploaded file, processes the data, and stores the processed data in a state variable.

    ```python theme={null}
    import io
    import pandas as pd
    import writer as wf

    if "input_file" in state:
        # Read the uploaded file into a buffer
        file_buffer = io.BytesIO(state["input_file"][0]["data"])
        # Read buffer into pandas DataFrame
        df = pd.read_csv(file_buffer)
        
        # Process the data to calculate the total revenue for each product category
        df['total_revenue'] = df['quantity'] * df['unit_price']
        df = df.groupby('product_category').agg({
            'total_revenue': 'sum',
            'quantity': 'sum'
        }).reset_index()
        
        # Set processed DataFrame in state
        state["processed_data"] = df
    ```
  </Step>
</Steps>

The blueprint should look like this, with the Python block containing the code to process the uploaded file and store the processed data in a state variable.

<img alt="" />

### Preview the agent

Navigate to the **Preview** tab to see the agent in action. You should see a button to upload a CSV file and a table to display the processed data.

Upload a CSV file; see the [beginning of this example](#example%3A-csv-file-upload-and-processing) for a sample CSV file.

Once you click the **Process CSV** button, the agent processes the data and displays it in the DataFrame component.

<img alt="" />

## Best practices

1. **Use pandas for data processing**: Clean, aggregate, and transform data before display
2. **Keep DataFrames reasonably sized**: Use "Display row count" to control how many rows show simultaneously
3. **Enable appropriate features**: Only enable editing if users should modify data
4. **Consider text wrapping**: Toggle "Wrap text" based on your data content
5. **Use meaningful column names**: Pandas column names become the table headers

## Next steps

Try extending this example by:

* Adding more sophisticated pandas operations such as `groupby` and `pivot_tables`
* Connecting to real databases or APIs
* Creating calculated columns based on business rules
* Styling the component with custom CSS classes


# Agent Builder Demo Application
Source: https://dev.writer.com/agent-builder/demo-agent

Explore the Agent Builder demo agent that analyzes customer reviews and drafts responses. Learn how UI, blueprints, and state variables work together.

<Warning>
  Agent Builder is in beta. Some features are still in development and are subject to change.
</Warning>

When you create a new agent with Agent Builder, it comes with a demo agent that analyzes customer reviews and drafts personalized responses. This walkthrough shows you how the demo agent works and how to modify it to understand Agent Builder's core concepts.

<img alt="" />

This walkthrough covers the following topics:

* How to navigate the Agent Builder interface
* How the UI and blueprint work together through state variables
* How to use the **Classification** block to route workflows
* How to connect blocks using `@{result}`
* How to modify an existing agent to add new behavior

<Tip>
  If you are unfamiliar with Agent Builder, review the [Agent Builder Overview](/agent-builder/overview) to learn more about the different components of an agent.
</Tip>

## Start an Agent Builder project

To create an agent with Agent Builder, log in to [AI Studio](https://app.writer.com/aistudio) and follow these steps:

<Steps>
  <Step title="Click the Build an agent button">
    Click the **Build an agent** button in the top right corner of the page.

    <img alt="Build an agent button" />
  </Step>

  <Step title="Create a new agent">
    In the modal that appears, select **New Agent** to create a new agent from scratch.

    <Tip>
      For common use cases, prebuilt agent templates are available. These include both general-purpose and industry-specific options, all customizable to your specific requirements.
    </Tip>

    <img alt="Agent creation options" />

    Once the agent is created, a new tab will open in your browser with the Agent Builder interface.
  </Step>

  <Step title="Return to the Agent Builder interface">
    You can get to the Agent Builder interface any time by going to the [AI Studio homepage](https://app.writer.com/aistudio) and selecting the agent you created.

    You'll see the Configure Deployment page when you select the agent. To get to the edit interface, click the **Edit** button in the top right corner.

    <img alt="Edit agent" />
  </Step>
</Steps>

## Preview the demo agent

Writer initializes the new agent with a demo workflow that you can use to get started. The demo agent takes a customer review and uses a series of tools to analyze the review and draft a response to the customer.

### Agent Builder views

The Agent Builder interface contains four views:

<img alt="" />

* **Interface**: The agent's UI, where you can edit the agent's appearance.
* **Blueprints**: The agent's blueprint, where you can edit the agent's behavior.
* **Vault**: The agent's secrets, where you can add secrets like API keys or passwords.
* **Preview**: Test the agent's behavior and preview what the user sees.

You can switch between views by clicking the tabs at the top of the page.

### Preview the agent's behavior

Before you start editing, preview the agent to see how it works:

<Steps>
  <Step title="Click the Preview tab">
    Click the **Preview** tab to see the agent in action.

    <img alt="" />
  </Step>

  <Step title="Copy and paste the example customer review">
    Copy the example customer review under **1. Copy Review** and paste it into the **2.Paste Review** text input.
  </Step>

  <Step title="Click Draft response">
    Click **Draft response**. You'll see a status message in the UI that the agent is generating a response. Once the agent has finished, you'll see the response in the UI.
  </Step>

  <Step title="Inspect the agent's blueprint">
    Click the **Blueprints** tab to see the agent's blueprint. You'll see green boxes highlighting the path that the agent took through the blueprint. You'll learn more about the blueprint in the next section.

    <img alt="" />
  </Step>
</Steps>

## Tour of the demo agent architecture

The demo agent consists of an interface and a blueprint. The interface and blueprint are connected through the agent's state and UI triggers.

<img alt="Demo agent blueprint structure showing the flow from UI trigger to text generation" />

* [**Interface**](#interface-view): What the user sees and interacts with.
* [**Blueprint**](#blueprints-view): A flowchart-like interface where you connect blocks of logic.
* [**Agent state**](#agent-state): A set of values that's shared between the UI and the blueprint. Both the UI and the blueprint can reference and update the state.
* **UI trigger**: A trigger from the UI that starts the agent's blueprint. In this case, the UI trigger is attached to click events on the **Draft response** button.

### Agent state

The [agent's state](/agent-builder/state) is a core component of the agent's behavior. It contains values that the UI and blueprint can access and update.

You can view the agent's state from any view by clicking the **State explorer** icon in the top right of the page.

<img alt="" />

The section [Connecting the UI and the blueprint](#connecting-the-ui-and-the-blueprint) describes how the UI and blueprint interact with the state to perform the agent's tasks.

### Blueprints view

The Blueprints view is where you define the agent's logic and behavior. It's a flowchart-like interface where you connect blocks of logic.

If you previewed the agent, you'll see green boxes highlighting the path that the agent took through the blueprint. For the sample review, the agent takes the following steps:

<img alt="" />

<Steps>
  <Step title="UI Trigger">
    The blueprint started with the **UI Trigger** element. In this case, the UI Trigger is configured to run when the user clicks the **Draft response** button in the UI.
  </Step>

  <Step title="Set state to show the progress message">
    The agent used the **Set state** block to set the `progress_message` value for the agent state to show that it is generating a response.
  </Step>

  <Step title="Classification">
    The agent uses the **Classification** block to analyze the review to determine the primary focus of the review, from the following: `Packaging`, `Pricing`, `Quality`, `Delivery`, or `Empty`.

    The agent determined the review fit into the `Quality` category. Classification blocks are built-in blocks that use a Palmyra model to classify the input based on a set of user-defined categories.
  </Step>

  <Step title="Text generation">
    The agent used the **Text generation** block to have a Palmyra model draft a response to the customer specifically focused on quality.
  </Step>

  <Step title="Set state for the response">
    The agent stored the drafted response using the `@{result}` environment variable so it could use it in the UI.
  </Step>

  <Step title="Set state to clear the progress message">
    The agent cleared the `progress_message` value in the state to remove the loading message from the UI.
  </Step>
</Steps>

#### View another path through the blueprint

To view another path through the blueprint, go back to the **Preview** view and paste the following:

`I like the style but it's a lot more expensive than other brands without any difference in quality.`

Click **Draft response**, and navigate back to the **Blueprint** view. You'll see that the agent took a different path through the blueprint, this time selecting the `Pricing` category and drafting a response using a pricing-related prompt.

### Interface view

The Interface view is where you define the agent's appearance. You build the interface with components that you can customize and reference in the blueprint.

This agent's UI has the following components:

* A welcome image in a sidebar
* A section for the review:
  * A **Text area Input** for the customer review
  * A button to **Draft response**
* A section for the response:
  * A **Message** component to display the in progress status message
  * A section with **Text** components to display the response

The Interface uses **Column container** components to layout the Review section in a two-column format.

<img alt="" />

#### Hidden sections

You won't see the hidden section for the results in the UI when you first load the agent. You can see that the section is in the UI from the **Interface Layers** view, which shows all the components in the UI. The hidden section has an icon next to it indicating that it's not visible.

<img alt="" />

Click that element in the Component tree to see its settings. Under **Visibility**, it's set to only show up once there is a value in the `review_response` state variable.

<img alt="" />

### Connecting the UI and the blueprint with state variables

To build full-featured agents, you must be able to connect the UI to the blueprint.

The agent's state is the main way to pass data between the UI and the blueprint. The state contains a set of values that the UI and blueprint can both reference and set. You can also use triggers to connect the UI to the blueprint.

Here are the places where the demo agent connects the UI to the blueprint and passes data between them:

<Steps>
  <Step title="UI Trigger">
    The **Draft response** button in the UI is connected to the **UI Trigger** element in the blueprint. Click events trigger the blueprint to run.
  </Step>

  <Step title="Customer review input">
    The **Text area Input** component in the UI binds the user's input to the `customer_review` state variable. The blueprint uses this value in the **Classification** block.
  </Step>

  <Step title="Progress message">
    The **Message** component in the UI displays the value of the `progress_message` state variable to the user. The blueprint sets this value after the user clicks **Generate response**.
  </Step>

  <Step title="Review response">
    The two sections in the response with **Text** components in the UI depend on the value of the `review_response` state variable.

    1. The blueprint sets this value after the **Text generation** block runs.
    2. Once `review_response` contains a value, the section that had the message "The response will be shown here" disappears.
    3. The section that displays the response appears. The text area in that section contains the value of `review_response`.

    See [Hidden sections](#hidden-sections) for more information about the review response sections and conditionally showing and hiding them.
  </Step>

  <Step title="Clear progress message">
    Once the review response is generated, the final **Set state** block in the blueprint sets the `progress_message` state variable to an empty string to clear the loading message from the UI.
  </Step>
</Steps>

### Passing data between blocks

The demo agent also shows how to pass data between blocks with the `@{result}` variable. Once the text generation block has finished, it passes its result to the next block in the blueprint, which is the **Set state** block. The **Set state** block sets the `review_response` state variable with the `@{result}` from the text generation block.

Learn more about all available variables in [Using Data from Previous Blocks](/agent-builder/execution-environment).

## Modify the demo agent

Now that you've toured the Agent Builder interface, try editing the agent's behavior.

### Add a new category to the classification block

This example shows how to add a new category to the classification block to look for and respond to reviews that focus on sizing issues.

First, add another category to the **Classification** block in the blueprint so that the agent can also classify reviews that are about sizing.

<Steps>
  <Step title="Open the Classification block">
    Navigate to the **Blueprints** view and click the **Classification** block in the blueprint to open the block settings.
  </Step>

  <Step title="Add a new category">
    In the **Categories** section, add a new category called `Sizing` with the value:

    ```
    The review relates to sizing issues or satisfaction with sizing.
    ```

    <img alt="" />
  </Step>

  <Step title="Add a new text generation block">
    Add a new **Text generation** block to the blueprint to generate a response to sizing-related reviews:

    1. Click one of the existing **Text generation** blocks in the blueprint.
    2. Type `Command + c` (on Mac) or `Ctrl + c` (on Windows) to copy the block.
    3. Click onto the blueprint canvas and type `Command + v` (on Mac) or `Ctrl + v` (on Windows) to paste the block.
  </Step>

  <Step title="Edit the new text generation block">
    Click on the new text generation block you pasted to edit it. Update the following settings:

    * **Alias**: `Draft sizing response`
    * **Prompt**: update the prompt to focus on sizing issues:

    ```
    Take the role of a customer success rep and draft an email response to the customer review below that mentions sizing: @{customer_review}

    Address the customer's sizing experience. Thank them if the sizing was accurate, or address their concerns if there were problems with sizing.
    ```
  </Step>

  <Step title="Connect the new text generation block to the rest of the blueprint">
    Connect the new text generation block to the rest of the blueprint:

    * Drag a line from the purple dot next to **Sizing** on the **Classification** block to connect it to the new **Text generation** block.
    * Drag a line from the **Success** connection point on the **Text generation** block to the **Set state** block.

      <img alt="" />
  </Step>
</Steps>

Now, preview the agent to see the new behavior. Paste the following review into the input field and click **Draft response**:

```
The sizing felt off, I'm normally a small and even the XS was too big.
```

You should see the agent take a different path through the blueprint and generate a sizing-related response.

### Add a new UI component

This next example shows how to add a new UI component that you can incorporate into the agent's blueprint.

The steps show how to prompt the agent to add a discount of a certain percentage when generating a response to the customer review. You will:

* Add a **Select input** component to the UI that lists different discount percentages
* Edit the prompt for one of the **Text generation** blocks to add the selected discount percentage to the response if there is one

**From the UI view:**

<Steps>
  <Step title="Add a new Select input component">
    Add a new **Select input** component to the UI. Drag it into the section that contains the area for pasting the review text and the **Draft response** button.

    Update the following settings in the Select input component's settings:

    * **Label**: `Discount %`
    * **Options**: `0`, `5`, `10`, `15` (the keys and values should be the same)
    * **Link variable** under **Binding**: `discount`
  </Step>

  <Step title="Move the component above the Draft response button">
    Move the component above the **Draft response** button by clicking the three vertical dots on the component's settings menu and selecting **Move up**. You can also drag the component up in the UI.

    <img alt="" />

    The UI should now look like this:

    <img alt="" />
  </Step>
</Steps>

**From the blueprint view:**

<Steps>
  <Step title="Edit the Draft packaging response text generation block">
    Click the **Draft packaging response** text generation block to open its settings.

    Edit the block's prompt to read the `discount` state variable and add a discount to the response if there is one. For example, add the following to the prompt:

    ```
    Offer a discount of @{discount} percent on their next purchase.

    Do not offer a discount if the value of @{discount} is 0.
    ```

    <img alt="" />
  </Step>
</Steps>

Now, preview the agent to see the new behavior. Paste the following review into the input field:

```
The packaging was awful.
```

Then, choose a discount amount from the select input and click **Draft response**.

You should see the agent take a different path through the blueprint and generate a response that offers a discount if you set it above 0.

You can update the prompts for the other text generation blocks to have the agent offer a discount for the other categories.

## Next steps

Now that you've interacted with and modified an agent in Agent Builder, you can build your own. Check out the [Agent Builder Quickstart](/agent-builder/quickstart) to learn how to build a new agent from scratch.

<feedback />


# Deploy with Docker
Source: https://dev.writer.com/agent-builder/deploy-with-docker

Deploy Agent Builder agents with Docker containers. Run agents on cloud platforms, on-premises servers, or local development machines.

This guide shows you how to deploy your Agent Builder agents using [Docker containers](https://www.docker.com/). After completing these steps, you can run your agents in any environment that supports Docker, including cloud platforms, on-premises servers, or local development machines.

<Tip>
  To deploy a local agent to the Writer cloud, see [Sync agents between local and cloud](/agent-builder/sync-agent).
</Tip>

## Prerequisites

Before getting started, ensure you have:

* [A local Agent Builder project](/agent-builder/local-development)
* [Docker installed on your system](https://docs.docker.com/get-docker/)
* [A Writer API key](/api-reference/api-keys)

## Overview

Below are the main steps to deploying your agent with Docker:

1. Create and build a Docker image for your agent
2. Publish the Docker image to a container registry and deploy it to your target environment

## Create a Docker image

A `Dockerfile` contains instructions that tell Docker how to build your agent's container image. The `Dockerfile` must be named `Dockerfile` and placed in your agent's project directory alongside `main.py` and `.wf/`.

### Create the Dockerfile

Create a `Dockerfile` in your agent's project directory with the following content:

```docker theme={null}
FROM python:3.13-slim

# Install system dependencies
RUN apt-get update -y && \
    apt-get install -y build-essential cmake python3-dev curl && \
    rm -rf /var/lib/apt/lists/* && \
    mkdir /app

# Workdir
WORKDIR /app
# Copy only requirements first for better caching, then install
COPY requirements.txt /app/requirements.txt
RUN python -m pip install --upgrade pip && \
    pip install --no-cache-dir -r /app/requirements.txt

# Copy the rest of the project
COPY . /app

# Set up the agent
ENTRYPOINT [ "writer", "run" ]
EXPOSE 8080
CMD [ ".", "--port", "8080", "--host", "0.0.0.0" ]
```

<Note>
  This Dockerfile uses an official Python base image and installs dependencies from the `requirements.txt` file that's automatically generated in your Agent Builder project. If you're familiar with Docker, you can customize this `Dockerfile` for your specific needs. Agent Builder agents are standard Python applications that work with the Writer Framework package.

  The `--port 8080` flag overrides Writer Framework's default port range (3005-3099 for run mode) to use port 8080, which is commonly used for web applications and works well with most cloud platforms.
</Note>

### Build the Docker image

Build your Docker image using the following command:

```bash theme={null}
docker build . -t my-agent-app
```

Replace `my-agent-app` with a descriptive name for your agent.

<Warning>
  By default, Docker builds images for the architecture it's running on. If you're using an ARM-based computer (like a Mac with M1/M2 chip or Raspberry Pi), Docker builds an ARM image. Most cloud platforms require x86 images. Use [Docker buildx](https://docs.docker.com/build/building/multi-platform/) to build multi-platform images, or build on an x86 machine.
</Warning>

## Run the container

Your agent needs access to your Writer API key to perform actions with the Writer API and Writer LLMs. The Docker image doesn't include your API key for security reasons, so you need to provide it when running the container.

### Set up your environment

Create a `.env` file in your project directory with your API key:

```bash theme={null}
WRITER_API_KEY=your_api_key_here
```

Then source the environment file in your shell:

```bash theme={null}
source .env
```

Now you can run your container using either `docker run` or `docker compose`.

### Using `docker run`

```bash theme={null}
docker run -e WRITER_API_KEY -p 8080:8080 my-agent-app

# Or load environment variables from a file:
docker run --env-file .env -p 8080:8080 my-agent-app
```

### Using `docker compose`

Create a `docker-compose.yml` file for easier management:

```yaml theme={null}
services:
  agent:
    build: .
    ports:
      - "8080:8080"
    environment:
      - WRITER_API_KEY
    # Or load environment variables from a file
    # env_file:
    #   - .env
```

Then run with:

```bash theme={null}
docker compose up
```

## Test your agent locally

After setting up your environment and building your Docker image, test your agent locally using either `docker run` or `docker compose`.

Open your browser to [http://localhost:8080](http://localhost:8080) to verify your agent is running correctly.

## Publish your Docker image

To deploy your agent to cloud platforms, publish your Docker image to a container registry such as Docker Hub, Google Container Registry, or Amazon Elastic Container Registry.

The following example shows how to publish your image to Docker Hub:

```bash theme={null}
docker tag my-agent-app <DOCKERHUB_USERNAME>/my-agent-app:latest
docker login
docker push <DOCKERHUB_USERNAME>/my-agent-app:latest
```

Once your image is in a registry, deploy it to services such as AWS ECS, Azure Container Instances, or Kubernetes.

## Troubleshoot issues

### Common issues

**Agent fails to start**

* Verify your `WRITER_API_KEY` is set correctly
* Check that all required dependencies are installed
* Review container logs for specific error messages

**Port binding errors**

* Ensure the port in your `Dockerfile` matches the port you're trying to access
* Check that the port isn't already in use by another service

**Memory or CPU issues**

* Increase resource allocation in your cloud platform
* Optimize your agent's code for better performance
* Consider using a more powerful instance type

### Debug locally

Run your container with verbose logging to debug issues:

```bash theme={null}
docker run -e WRITER_LOG_LEVEL=DEBUG -p 8080:8080 my-agent-app
```

## Next steps

* Learn how to [sync your local agent with cloud agents](/agent-builder/sync-agent) for easier deployment
* Explore [custom Python code](/agent-builder/python-code) to extend your agent's capabilities
* Set up [secrets management](/agent-builder/secrets) for production deployments in the Writer cloud

<feedback />


# Using data from previous blocks
Source: https://dev.writer.com/agent-builder/execution-environment

Pass data between blocks in Agent Builder blueprints. Access state variables, secrets, and results from preceding blocks with execution environment.

Agents can pass data between blocks in the blueprint and the UI to create dynamic workflows. Each block operates with a set of variables in its execution environment.

The execution environment includes the agent's [state variables](/agent-builder/state) and [secrets](/agent-builder/secrets), along with the results and inputs from the preceding blocks in the blueprint. You can access variables from the execution environment in the blueprint and in custom code.

The most common use case for the execution environment is to access the result of the preceding block. Each blueprint block that runs passes its result to the next block in the blueprint, which you can access with the `@{result}` variable in the following block.

The execution environment is specific to each block in the blueprint and changes based on the block's inputs, outputs, and state.

This document includes:

* [An explanation of the execution environment](#execution-environment-explained)
* [A table of variables available in the execution environment](#variables-available-to-each-block)
* [Examples of how to access variables in the execution environment](#access-execution-environment-variables)

## Execution environment explained

Think of an execution environment like a toolbox that a block can access while it's running. This toolbox contains useful information that your agent needs to do its job, like remembering information from earlier steps or accessing important values you've saved. Blocks can add, remove, and update variables in the execution environment as they run.

Most of the information in the toolbox is primarily useful for internal use, debugging, and advanced use cases, but `@{result}` is particularly important for you as you build workflows.

Whenever a block finishes running, it adds its result to the execution environment. This result is then available to the next block in the blueprint so it can act on it.

<img alt="" />

### Example from a blueprint

Consider the following section of a blueprint that generates text based on a specific prompt. It shows two connected blocks:

* A **Text generation** block that generates text based on a specific prompt
* A **Set state** block that sets a state variable with the results of the **Text generation** block

<img alt="" />

* The **Text generation** block uses the state variable `product` to access information that the user input via the UI. It then generates text based on that information.
* The **Set state** block's execution environment includes the `result` variable from the **Text generation** block, which is the summary of the file. It sets a state variable with that result so that the UI can display the summary.

## Variables available to each block

The following table shows the variables that are available to each block in the execution environment. Not all variables are available in all blocks; for example, the `api_calls` variable is only available when the block makes an API call.

Many of these variables are primarily useful for internal use, debugging, and advanced use cases. However, the `result` variable is particularly useful for building workflows.

| Variable         | Type          | Description                                                                                                                                                                                                                                       | Example                                                                                                                      |
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `api_calls`      | `array[dict]` | Any Writer API calls that the agent made at this block, including the request and the response. For example, API calls from a **Text generation** block to the Writer [chat completions](/api-reference/completion-api/chat-completion) endpoint. | `[{'request': {'method': 'POST', ...}, 'response': {'status_code': 200, ...}}]`                                              |
| `call_stack`     | `dict`        | The call stack of the agent. The key is the index of the block in the call stack and the value is the block ID.                                                                                                                                   | `{0: "123abc", 1: "456def"}`                                                                                                 |
| `context`        | `dict`        | The ID of the block that triggered the blueprint execution and the event that triggered it.                                                                                                                                                       | `{'target': '123abc', 'event': 'wf-click'}`                                                                                  |
| `httpx_requests` | `array[dict]` | Any HTTP requests that the agent made at this block, including the request and the response.                                                                                                                                                      | `[{'request': {'method': 'POST', ...}, 'response': {'status_code': 200, ...}}]`                                              |
| `item`           | Any           | An individual item in a **For-each** block loop. The type of the item varies based on the values provided to the **For-each** block. For a dictionary, this is the value of the item. For a list, this is the item itself.                        | `file123`                                                                                                                    |
| `itemId`         | `str`         | The ID of the individual item in a **For-each** block loop. For a list, this is its index in the loop, starting at 0. For a dictionary, this is the key of the item.                                                                              | `0`                                                                                                                          |
| `message`        | `str`         | The error message if a block failed with an error.                                                                                                                                                                                                | `"Invalid input: age must be a number"`                                                                                      |
| `result`         | varies        | The result of the preceding block. The type of the result varies based on the block.                                                                                                                                                              | `Thank you so much for your wonderful review! We're thrilled to hear that you've found the perfect tailored blazer with us.` |
| `results`        | `dict`        | The full list of results from all blocks in the blueprint. It's a dictionary where the key is the block ID and the value is the result of the block. If the block hasn't run yet, the value is `null`.                                            | `{'123abc': 'Thank you so much...', '456def': '%Generating response...', '789ghi': null}`                                    |
| `session`        | `dict`        | Session information, such as the session ID, cookies, headers, and user information.                                                                                                                                                              | `{'id': '123', 'cookies': {...}, 'headers': {...}, 'userInfo': {}}`                                                          |
| `state`          | `dict`        | The agent's [state variables](/agent-builder/state).                                                                                                                                                                                              | `{'user_name': 'John', 'persona': 'sales'}`                                                                                  |
| `target`         | `str`         | The ID of the next block to run.                                                                                                                                                                                                                  | `'123abc'`                                                                                                                   |
| `ui`             | `object`      | The UI of the agent as a WriterUIManager object. This is a Python object that allows you to interact with the UI of the agent via custom code.                                                                                                    |                                                                                                                              |

## Access execution environment variables

### In blueprint blocks

You can read the execution environment variables in blueprint blocks using `@{variable_name}` syntax. For example, `@{result}` accesses the result of the preceding block.

This example shows a **Set state** block that uses the `@{result}` variable to access the result of the preceding block. The block sets a state variable `message` with the string `Result: ` and the output of the preceding block.

<img alt="" />

#### Nested variable syntax

You can access nested variables in blueprint blocks using dot notation. For example, `@{result.0.id}` accesses the `id` field of the first result of the preceding block.

This example shows a **Set state** block to set a `file_id` state variable from an **Add files to Writer Cloud** block. It uses `@{result.0.id}` to access the `id` field of the first item in the result of the preceding block.

Here is the what the `result` variable looks like in the execution environment. It's possible to add multiple files to Writer Cloud, so the `result` variable from this block is an array of dictionaries. The `0` index accesses the first item in the array.

<img alt="" />

The **Set state** block uses `@{result.0.id}` to access the `id` field of the first item in the result of the preceding block.

<img alt="" />

### In custom Python code

You can access the execution environment variables in custom Python code directly as variables.

For example, this Python block in the blueprint accesses the output of the preceding block and prepends the string `Result: ` to it. It then sets state variable `message` with the final result.

<img alt="" />

#### Environment variables

Your `WRITER_API_KEY` is also available when you run custom Python code. It's available as an environment variable (as opposed to an execution environment variable), so you can access it using `os.getenv('WRITER_API_KEY')`.

```python theme={null}
import os

api_key = os.getenv('WRITER_API_KEY')
```

<feedback />


# Process invoices and send to Slack
Source: https://dev.writer.com/agent-builder/invoice-processing



This tutorial walks through building an agent that processes PDF invoices, extracts structured data, and sends the results to Slack. The agent takes an uploaded invoice PDF, extracts key information like vendor details and line items, and posts a formatted summary to a Slack channel.

<CardGroup>
  <Card title="Interface">
    <img alt="Invoice processing interface" />
  </Card>

  <Card title="Blueprint">
    <img alt="Invoice processing blueprint" />
  </Card>

  <Card title="Slack message">
    <img alt="Invoice processing Slack message" />
  </Card>
</CardGroup>

This pattern is useful for automating accounts payable workflows, expense processing, and any scenario where you need to extract structured data from documents and route it to business systems. Once you complete this tutorial, you can continue enhancing the agent by adding [tool calling](/agent-builder/tool-calling-tutorial) with behaviors like validating invoices, flagging potential issues, and comparing invoices against historical data.

## Prerequisites

Before building this agent, you need to set up a Slack webhook to send the invoice data to a Slack channel.

Follow the instructions in the [Slack documentation](https://api.slack.com/messaging/webhooks) to set up a webhook. The general steps are:

1. Go to [Slack API](https://api.slack.com/apps) and create a new app
2. Select "From scratch" and provide a name for your app
3. Choose the Slack workspace where you want to build the agent
4. Navigate to "Incoming Webhooks" in the left sidebar of the new app and activate webhooks
5. Click "Add New Webhook to Workspace" and select the channel where invoices should be posted. You might need administrator permissions to add a webhook to a channel, depending on your Slack workspace settings.
6. Copy the webhook URL - you'll need this for the HTTP request block. The webhook URL will look like: `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX`

## Agent overview

This agent processes invoices through the following steps:

1. User uploads a PDF invoice file
2. File is uploaded to Writer Cloud storage
3. PDF content is extracted and parsed
4. AI extracts structured invoice data such as vendor, amounts, line items.
5. Formatted invoice summary is sent to Slack

## Build the UI

The agent's UI contains a file input for uploading invoices and a submit button.

<img alt="UI" />

<Steps>
  <Step title="Add a file input">
    Drag a **File Input** block to the canvas. In the block's configuration menu, update the following:

    * **Label**: `Invoice`
    * **Allowed file types**: `.pdf`
    * **Link variable** under **Binding**: `invoice`

    This allows users to upload PDF files and stores the file in the `invoice` state variable.
  </Step>

  <Step title="Add a button to submit the request">
    Drag a **Button** block to the canvas. In the block's configuration menu, update the following:

    * **Label**: `Submit`
  </Step>
</Steps>

## Build the blueprint

The logic of the blueprint is as follows:

1. The **UI Trigger** block starts the agent when the user clicks the submit button
2. The **Add files to Writer Cloud** block uploads the invoice file to cloud storage
3. The **Parse PDF** block extracts text content from the uploaded file
4. The **Structured Output** block processes the PDF text and extracts invoice data as JSON
5. The **HTTP Request** block sends the structured data to Slack

The finished blueprint contains the following blocks:

<img alt="Finished blueprint" />

<Steps>
  <Step title="Add a UI Trigger block">
    Drag a **UI Trigger** block to the canvas. In the block's configuration menu, update the following:

    * **Component Id**: select the **Submit** button from the dropdown
    * **Trigger**: `wf-click`

    This triggers the blueprint when the user clicks the Process Invoice button.
  </Step>

  <Step title="Add an Add files to Writer Cloud block">
    Drag an **Add files to Writer Cloud** block to the canvas. In the block's configuration menu, update the following:

    * **Files**: `@{invoice}`

    This uploads the invoice file from the `invoice` state variable to Writer Cloud storage.
  </Step>

  <Step title="Add a Parse PDF block">
    Drag a **Parse PDF tool** block to the canvas. In the block's configuration menu, update the following:

    * **File**: `@{result.0.id}`

    This extracts the text content from the uploaded PDF file using the file information returned from the Add files to Writer Cloud block.
  </Step>

  <Step title="Add a Structured Output block">
    Drag a **Structured Output** block to the canvas. In the block's configuration menu, update the following:

    * **Prompt**:

    ```
    Extract the following information from this invoice:

    - Invoice number
    - Vendor name and address
    - Invoice date and due date
    - Bill-to company and address  
    - Total amount, subtotal, and tax amount
    - Currency
    - All line items with descriptions, quantities, unit prices, and totals
    - Payment terms
    - Purchase order number (if present)

    If any information is not clearly stated in the invoice, use null for that field.
    ```

    * **Input**: `@{result}`
    * **Model**: `Palmyra X5`
    * **JSON Schema**: The following is an example JSON schema for the structured output. You can use this as a starting point, or create your own schema based on the invoice format you'd like to extract.

    ```json theme={null}
    {
      "$schema": "http://json-schema.org/draft-07/schema#",
      "type": "object",
      "title": "Invoice Processing Data",
      "description": "Schema for structured invoice data extracted from PDF documents",
      "properties": {
        "invoiceNumber": {
          "type": "string",
          "description": "Unique invoice identifier"
        },
        "vendorName": {
          "type": "string",
          "description": "Name of the company or vendor issuing the invoice"
        },
        "vendorAddress": {
          "type": "string",
          "description": "Full address of the vendor"
        },
        "invoiceDate": {
          "type": "string",
          "format": "date",
          "description": "Date the invoice was issued (YYYY-MM-DD format)"
        },
        "dueDate": {
          "type": "string",
          "format": "date",
          "description": "Payment due date (YYYY-MM-DD format)"
        },
        "billToCompany": {
          "type": "string",
          "description": "Company name being billed"
        },
        "billToAddress": {
          "type": "string",
          "description": "Billing address"
        },
        "totalAmount": {
          "type": "number",
          "description": "Total invoice amount including tax",
          "minimum": 0
        },
        "subtotal": {
          "type": "number",
          "description": "Subtotal before tax",
          "minimum": 0
        },
        "taxAmount": {
          "type": "number",
          "description": "Total tax amount",
          "minimum": 0
        },
        "currency": {
          "type": "string",
          "description": "Currency code (e.g., USD, EUR, GBP)",
          "default": "USD"
        },
        "lineItems": {
          "type": "array",
          "description": "Individual items or services on the invoice",
          "items": {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "description": "Description of the product or service"
              },
              "quantity": {
                "type": "number",
                "description": "Quantity of items",
                "minimum": 0
              },
              "unitPrice": {
                "type": "number",
                "description": "Price per unit",
                "minimum": 0
              },
              "totalPrice": {
                "type": "number",
                "description": "Total price for this line item",
                "minimum": 0
              }
            },
            "required": ["description", "totalPrice"],
            "additionalProperties": false
          }
        },
        "paymentTerms": {
          "type": "string",
          "description": "Payment terms (e.g., 'Net 30', 'Due on receipt')"
        },
        "purchaseOrderNumber": {
          "type": "string",
          "description": "Purchase order number if referenced on invoice"
        }
      },
      "required": [
        "invoiceNumber",
        "vendorName",
        "invoiceDate",
        "totalAmount",
        "currency",
        "lineItems"
      ],
      "additionalProperties": false
    }
    ```

    This processes the PDF text and extracts structured invoice data according to the JSON schema. The AI will identify and extract the relevant information from the invoice text.
  </Step>

  <Step title="Add an HTTP Request block to send to Slack">
    Drag an **HTTP Request** block to the canvas. In the block's configuration menu, update the following:

    * **Method**: `POST`
    * **URL**: Your [Slack webhook URL](/agent-builder/invoice-processing#prerequisites)
    * **Body**:

    ```json theme={null}
    {
      "text": "New Invoice Processed",
      "blocks": [
        {
          "type": "header",
          "text": {
            "type": "plain_text",
            "text": "📄 Invoice Processed"
          }
        },
        {
          "type": "section",
          "fields": [
            {
              "type": "mrkdwn",
              "text": "*Invoice #:* @{result.invoiceNumber}"
            },
            {
              "type": "mrkdwn",
              "text": "*Vendor:* @{result.vendorName}"
            },
            {
              "type": "mrkdwn",
              "text": "*Date:* @{result.invoiceDate}"
            },
            {
              "type": "mrkdwn",
              "text": "*Due Date:* @{result.dueDate}"
            },
            {
              "type": "mrkdwn",
              "text": "*Amount:* @{result.currency} @{result.totalAmount}"
            },
            {
              "type": "mrkdwn",
              "text": "*Bill To:* @{result.billToCompany}"
            }
          ]
        }
      ]
    }
    ```

    This sends a formatted message to Slack with the key invoice details. The message uses Slack's block kit format to create a structured, readable invoice summary.
  </Step>
</Steps>

## Preview the agent

Navigate to the **Preview** tab to test the agent.

Upload a PDF invoice and click the Process Invoice button. The agent will upload the file, extract the invoice data, and send a summary to your configured Slack channel.

You can see the agent's progress in the **Logs** tab, including the extracted JSON data and the Slack API response.

You should see a message in the Slack channel with the invoice data.

<img alt="Slack message" />

## Add tool calling for invoice validation

You can enhance this agent by adding [tool calling](/agent-builder/tool-calling) to validate invoices and flag potential issues. Here are some ways to extend the functionality:

* Check for missing required fields like tax ID or payment terms
* Compare against previous invoices from the same vendor to detect price discrepancies
* Validate amounts against purchase orders and company spending policies
* Flag duplicate invoice numbers or unusual payment terms
* Verify vendor details against your approved vendor list

To add these capabilities:

1. Add a tool calling block after the structured output to analyze the extracted data
2. Configure validation rules and checks in your custom Python code
3. Update the Slack message to include any validation warnings or approvals
4. Optionally trigger different workflows based on the validation results

Check out the [tool calling tutorial](/agent-builder/tool-calling-tutorial) to learn how to add these validation capabilities to your invoice processing agent.

<feedback />


# Create and test agents locally
Source: https://dev.writer.com/agent-builder/local-development

Develop Agent Builder agents offline on your local machine with Writer Framework. Test agents locally before deploying to Writer Cloud.

This guide shows you how to create, edit, and test agents on your local machine before deploying them to the Writer Cloud. This uses [Writer Framework](https://github.com/writer/writer-framework) to work with Agent Builder agents locally with Python.

After completing these steps, you can develop Agent Builder agents offline locally and push them to the cloud when you're ready to share them with your team.

For more information about syncing agents between local and cloud and deploying to the Writer Cloud, see [Sync agents between local and cloud](/agent-builder/sync-agent).

## Prerequisites

Before getting started, ensure you have:

* Python 3.8 or later
* `pip` package manager
* A Writer API key. Learn how to [create and manage API keys](/api-reference/api-keys).

### Set your Writer API key as an environment variable

To pass your API key to Writer Framework, you need to set an environment variable called `WRITER_API_KEY`.

Create a new file called `.env` in the root of your project, for example, `my-agent/.env`, and add the following line:

```bash theme={null}
export WRITER_API_KEY=[your_api_key]
```

Run the following command to activate the environment variable:

```bash theme={null}
source .env
```

### Install the Writer Framework package

Install the Writer Framework package in a virtual environment. The Writer Framework package includes tools for creating, editing, and testing agents locally and deploying them to the Writer Cloud.

```bash theme={null}
python3 -m venv venv  # Create a virtual environment
source venv/bin/activate  # Activate the virtual environment
pip install writer  # Install Writer Framework
```

<Tip>To make sure your local development environment matches the Writer Cloud environment, you should use the latest version of the Writer Framework package and check for updates regularly.</Tip>

## Create a new local project

Create a new Agent Builder project locally using the Writer Framework CLI:

```bash theme={null}
writer create my-agent
cd my-agent
```

This creates a new project directory with the following structure:

```
my-agent/
├── .wf/                    # Project metadata and components. Don't edit this directory manually.
├── main.py                 # Main application code
├── requirements.txt        # Python dependencies
├── README.md              # Project documentation
└── static/                # Static assets
```

## Start developing

To run your agent in development mode with live reloading, run the following command:

```bash theme={null}
writer edit . # Run in the current directory
```

This command displays the local URL in your terminal, which you can use to access your agent. The default URL is `http://127.0.0.1:4005` and you can specify a different port with the `--port` flag.

<Note>When you run the `writer edit` command, you might see the warning `Missing required environment variables for vault access`. This warning is expected because Vault is a secrets management feature that's only available in the Writer Cloud version of Agent Builder.</Note>

### Production mode

To run your agent in production-like mode, run the following command:

```bash theme={null}
writer run .  # Run in the current directory
```

### Work with the included demo agent

When you create a new Agent Builder project, it automatically loads with a demo agent that demonstrates different components of an Agent Builder project. Learn how to navigate the demo agent in the [demo agent walkthrough](/agent-builder/demo-agent) or see how to [delete the demo agent in the Quickstart](/agent-builder/quickstart#clear-the-demo-agent).

### Control log output

You can enable verbose logging to see more detailed information about the agent's execution.

```bash theme={null}
writer edit . --verbose
```

If you find the logs too verbose by default, you can control the log level using the `WRITER_LOG_LEVEL` environment variable:

```bash theme={null}
# Show only errors
export WRITER_LOG_LEVEL=ERROR

# Reduce log output (only warnings and errors)
export WRITER_LOG_LEVEL=WARNING

# Show all logs (default)
export WRITER_LOG_LEVEL=INFO

# Show debug information (most verbose)
export WRITER_LOG_LEVEL=DEBUG
```

## Understand your project structure

Your local project contains several key components:

### Main application file (`main.py`)

The `main.py` file contains your agent's Python code and initial state:

```python theme={null}
import writer as wf

# Initialize the agent's state
initial_state = wf.init_state({
    "user_count": 0,
    "messages": []
})

# Define your agent's logic here
def handle_user_message(state, payload):
    # Process user input
    user_message = payload.get("message", "")
    state["messages"].append(user_message)
    state["user_count"] += 1
    
    return {"response": f"Processed message: {user_message}"}
```

### Project metadata (`.wf/` directory)

The `.wf/` directory contains your agent's configuration, including:

* UI components and layout
* Blueprint definitions
* State schemas
* Component configurations

Do not manually edit the `.wf/` directory. Writer Framework automatically manages this directory.

### Static assets (`static/` directory)

Store your images, CSS files, and other static assets in the `static/` directory. The development server serves these files automatically, allowing you to reference them in your agent's UI.

## Follow the development workflow

### 1. Start local development

```bash theme={null}
writer edit my-agent
```

The default URL to access your agent is `http://127.0.0.1:4005`. You can see the URL in the terminal after running the `writer edit` command.

### 2. Make changes

Edit your Python code, UI components, or blueprints either in code or in the local development web editor. The development server automatically detects changes and reloads your agent.

### 3. Test locally

Open your browser to the local development URL to test your agent's behavior.

### 4. Sync to cloud and deploy

When you're ready to make your agent live, sync it to the cloud and deploy it from there. See [Sync agents between local and cloud](/agent-builder/sync-agent) for complete instructions.

## Plan for cross-environment development

If you plan to do both local and cloud development or to deploy your agent to the Writer Cloud, review the following differences between the two environments.

### Python package compatibility

If you install Python packages locally that aren't listed in [Python libraries installed in Agent Builder](/agent-builder/python-libraries), your agent won't run in the cloud version of Agent Builder because those packages aren't available there.

### Local and cloud secrets support

The remote cloud version of Agent Builder uses [Vault](/agent-builder/secrets) to manage secrets. Vault is only available when you're working in the cloud editor. To access secrets and environment variables in local development, you should add them to your `.env` file.

<Warning>When you export your agent to a zip file, the exported file includes all files in your project including dot files. Do not include sensitive information in the exported file as it will be visible in the cloud editor. For example, you should delete your `.env` file when exporting your agent to a zip file and move your secrets to your cloud agent's Vault.</Warning>

If you plan to do both local and cloud development or to deploy your agent to the Writer Cloud, structure your code to check for both environment variables and Vault values. The code below shows how to get a secret from Vault if available, otherwise fall back to environment variables.

```python theme={null}
import os

def get_secret(key, default=None):
    """Get a secret from Vault if available, otherwise from environment variables"""
    try:
        # Try to get from Vault first (cloud environment). The `vault` object is available in Python code blocks and in event handlers.
        return vault[key]
    except KeyError:
        # Fall back to environment variables (local development)
        return os.getenv(key, default)

# Usage example
api_key = get_secret("WRITER_API_KEY", "default_key")
database_url = get_secret("DATABASE_URL")
```

### Keep your local Writer Framework package up to date

When you sync your agent to the cloud, the cloud version of Agent Builder uses the latest version of the Writer Framework package. If you're using a different version of the Writer Framework package in your local project, you might encounter errors when you sync to the cloud. To avoid this, keep your local Writer Framework package up to date by running the following command:

```bash theme={null}
pip install --upgrade writer
```

## Next steps

* Learn how to [sync your local agent with cloud agents](/agent-builder/sync-agent)
* Understand how to [deploy your agent](/agent-builder/sync-agent#deploy-your-agent) from the cloud
* Explore [custom Python code](/agent-builder/python-code) for advanced functionality

<feedback />


# Learn about Agent Builder
Source: https://dev.writer.com/agent-builder/overview

Build AI agents with a low-code visual editor. Collaborate with drag-and-drop blocks for tool calling, RAG, and custom workflows.

<Warning>
  Agent Builder is in beta. Some features are still in development and are subject to change.
</Warning>

<iframe title="Your Video Title" />

Agent Builder is a low-code tool for building AI agents with Writer. It enables technical and business builders to collaborate in a shared development environment, where they can assemble an agent using a visual editor and a library of drag-and-drop blocks.

<img alt="Agent Builder" />

Agents created in Agent Builder are fully composable and reusable—from the backend logic to frontend experience—making them faster to build and easier to maintain. Each agent consists of two core components: a blueprint and a UI.

* **Blueprint**: a visual map of the agent's business logic and behavior.
  * Blueprints are created using a library of configurable blocks for tool calling, built-in RAG, text generation, classification, state management, persistent key-value storage, and more.
  * They define how the agent processes input, makes decisions, and takes action—enabling it to reliably orchestrate work across people, data, systems, and even other agents built in Writer.
* **UI**: the interface that users interact with.
  * UIs are designed using elements like input fields, buttons, and embeds, including support for guided workflows with pagination.
  * They give users a structured, intuitive way to engage with agents, from simple chat interfaces to rich, interactive tools.

Agent Builder includes other features to help you build and deploy agents:

* **Custom code**: add custom Python code to the agent to extend its capabilities with more complex logic.
* **Deploy and supervise**: deploy the agent to the Writer cloud, grant access to specific teams, and monitor the agent's performance.

<CardGroup>
  <Card title="Blueprint">
    Define the agent's business logic and behavior.

    <img alt="Blueprint" />
  </Card>

  <Card title="UI">
    Design the agent's interface.

    <img alt="UI" />
  </Card>

  <Card title="Custom code">
    Add custom Python code to the agent to extend its capabilities.

    <img alt="Custom code" />
  </Card>

  <Card title="Deploy and supervise">
    Deploy the agent to the Writer cloud and monitor its performance.

    <img alt="Deploy and monitor" />
  </Card>
</CardGroup>

## Choose your development approach

Agent Builder supports two development approaches:

* **Cloud development**: Build agents directly in the web editor with immediate deployment
* **Local development**: Develop agents locally with your preferred editor, then sync to cloud when ready or [deploy with Docker](/agent-builder/deploy-with-docker)

Get started with Agent Builder with the following guides:

* [Tour the Agent Builder interface and interact with the demo agent](/agent-builder/demo-agent)
* [Build your first agent](/agent-builder/quickstart)
* [Develop agents locally](/agent-builder/local-development)

<feedback />


# Add custom Python code
Source: https://dev.writer.com/agent-builder/python-code



Agent Builder provides two ways to add custom Python code to your agents: using **Python code** blocks in your blueprint for inline logic, and using the code editor to create reusable functions and manage files.

## When to use custom Python code

Custom Python code is ideal for:

* **Complex data processing** that goes beyond built-in blocks
* **API integrations** with external services
* **Custom business logic** specific to your use case
* **File processing** and data transformation
* **Mathematical calculations** and statistical analysis
* **Reusable functions** across multiple blocks

## Variables available in Python code and blocks

The following variables and libraries are available in Python code blocks and other Python files. You can reference them in your code directly.

* **State variables**: Access your [agent's state](/agent-builder/state) using `state["variable_name"]`.
* **Execution environment**: Access [environment variables](/agent-builder/execution-environment) like `result`, `payload`, `session`.
* **Pre-installed libraries**: Use any of the [pre-installed Python libraries](/agent-builder/python-libraries) like `pandas`, `requests`, or `numpy`.
* **Custom functions**: Call functions you've defined in your `main.py` file from Python code blocks.
* **Secrets**: Access sensitive data from [Vault](/agent-builder/secrets) using `vault["SECRET_NAME"]`.

<Warning>
  You can't install additional Python packages in the Agent Builder code editor. Use the [pre-installed libraries](/agent-builder/python-libraries) that are already available. Request new packages in the [feature request form](https://writerai.atlassian.net/jira/software/form/a2dfaf0b-fcbf-4fdc-94af-8e24ed0c4fe1).
</Warning>

## Python blocks in blueprints

[**Python code** blocks](/blueprints/pythoncode) allow you to run custom code directly within your blueprint workflow. They're useful for one-off operations or processing steps that need to happen at specific points in your agent's logic.

<img alt="Python code block" />

### How to use Python code blocks

1. **Add a Python block** to your blueprint from the blocks library
2. **Write your code** in the Code field
3. **Access variables** directly from the [execution environment](/agent-builder/execution-environment), [state](/agent-builder/state), and [secrets](/agent-builder/secrets)
4. **Return results** using the `set_output()` function

### Returning values

Use `set_output(value)` to pass data to the next block:

```python theme={null}
# Calculate something
total = sum(state["numbers"])

# Pass it to the next block
set_output(total)
```

The value becomes available as `@{result}` in subsequent blocks.

## Code editor for custom functions

The **code editor** lets you create reusable Python functions and manage files for your agent. This is where you build the foundation that your Python blocks can use. Any functions you define in the code editor are available to use in Python code blocks.

<img alt="Code editor" />

### Accessing the code editor

1. **Open your agent** in Agent Builder
2. **Look for the code editor** at the bottom of the interface
3. **Click to expand** and start editing your `main.py` file

### File structure

Your agent starts with these files:

* **`main.py`**: your main Python code file.
* **`requirements.txt`**: shows the version of the `writer` package that your agent is using.
* **`README.md`**: documentation for your agent.
* **`static/`**: folder for static assets like images.

### Managing files

**Add new files**: click **+ Add file** to create Python modules, data files, or configuration files.

**Upload files**: click **Upload** to add external files like CSV data, images, or documents.

#### Creating folders and moving files

To move a file into a folder, click **+ Add file** and rename the file to `<folder_name>/<file_name>.<file_extension>`. If the folder doesn't exist yet, it will be created automatically.

For example, given the following file structure:

```
main.py
README.md
email_validator.py
pricing_calculator.py
```

To move `email_validator.py` to a new folder called `utils`, rename the file to `utils/email_validator.py` and click **Save file**. The new file structure will look like this:

```
main.py
README.md
pricing_calculator.py
utils/
  └── email_validator.py
```

To move a file into an existing folder, follow the same process as creating a new folder. For example, to move `pricing_calculator.py` to the `utils` folder, rename the file to `utils/pricing_calculator.py` and click **Save file**. The new file structure will look like this:

```
main.py
README.md
utils/
  ├── email_validator.py
  └── pricing_calculator.py
```

## Common patterns

### Initialize state variables

```python theme={null}
import writer as wf

initial_state = wf.init_state(
    user_id="123456",
    user_name="John Doe",
    user_email="john.doe@example.com"
)
```

### Data validation and cleaning

```python theme={null}
def clean_user_input(raw_data):
    cleaned = {}
    for key, value in raw_data.items():
        if isinstance(value, str):
            cleaned[key] = value.strip().lower()
        else:
            cleaned[key] = value
    return cleaned
```

### Using secrets in custom functions

```python theme={null}
import requests

def fetch_user_profile(user_id):
    """Fetch user profile from external API using stored credentials"""
    try:
        api_key = vault["USER_API_KEY"]
        headers = {"Authorization": f"Bearer {api_key}"}
        response = requests.get(
            f"https://api.example.com/users/{state['user_id']}",
            headers=headers
        )
        response.raise_for_status()
        return {"success": True, "profile": response.json()}
    except requests.RequestException as e:
        return {"success": False, "error": str(e)}
```

### File processing

```python theme={null}
import io
import pandas as pd

def process_uploaded_csv(file_data):
    try:
        file_buffer = io.BytesIO(file_data)
        df = pd.read_csv(file_buffer)
        summary = {
            "row_count": len(df),
            "columns": list(df.columns),
            "summary_stats": df.describe().to_dict()
        }
        return {"success": True, "summary": summary}
    except Exception as e:
        return {"success": False, "error": str(e)}
```

## Debugging

Use print or logger statements to debug your code. The [`logger` object](https://docs.python.org/3/library/logging.html) is globally available in Python blocks.

```python theme={null}
logger.info(f"Processing user: {state['user_id']}")
logger.info(f"Input data: {payload}")

# Your logic here
result = process_data()

print(f"Result: {result}")
set_output(result)
```

See more general troubleshooting tips in [Troubleshooting](/agent-builder/troubleshooting).

## Next steps

* Review [pre-installed Python libraries](/agent-builder/python-libraries) to see what's available
* Learn about [storing secrets](/agent-builder/secrets) for secure API key management
* Check out the [Python block reference](/blueprints/pythoncode) for detailed field information
* Learn about [state management](/agent-builder/state) for better data handling
* Review [execution environment variables](/agent-builder/execution-environment) for accessing block results

<feedback />


# Python libraries installed in Agent Builder
Source: https://dev.writer.com/agent-builder/python-libraries

View all Python packages available in Agent Builder including `aiohttp`, `boto3`, `pandas`, `requests`, and 50+ other libraries with version numbers.

Agent Builder includes the following Python packages:

| Package                                                                              | Version    |
| ------------------------------------------------------------------------------------ | ---------- |
| [`aiofiles`](https://pypi.org/project/aiofiles/)                                     | 24.1.0     |
| [`aiohttp`](https://pypi.org/project/aiohttp/)                                       | 3.11.16    |
| [`authlib`](https://pypi.org/project/authlib/)                                       | 1.6.5      |
| [`beautifulsoup4`](https://pypi.org/project/beautifulsoup4/)                         | 4.13.3     |
| [`boto3`](https://pypi.org/project/boto3/)                                           | 1.38.30    |
| [`click`](https://pypi.org/project/click/)                                           | 8.1.7      |
| [`cowsay`](https://pypi.org/project/cowsay/)                                         | 6.1        |
| [`docusign-esign`](https://pypi.org/project/docusign-esign/)                         | 5.0.0      |
| [`docxcompose`](https://pypi.org/project/docxcompose/)                               | 1.4.0      |
| [`fastapi`](https://pypi.org/project/fastapi/)                                       | 0.120.4    |
| [`fitz`](https://pypi.org/project/fitz/)                                             | 0.0.1.dev2 |
| [`gitignore-parser`](https://pypi.org/project/gitignore-parser/)                     | 0.1.12     |
| [`google-auth-oauthlib`](https://pypi.org/project/google-auth-oauthlib/)             | 1.2.2      |
| [`google-genai`](https://pypi.org/project/google-genai/)                             | 1.19.0     |
| [`gspread`](https://pypi.org/project/gspread/)                                       | 6.2.1      |
| [`ipython`](https://pypi.org/project/ipython/)                                       | 9.1.0      |
| [`jinja2`](https://pypi.org/project/jinja2/)                                         | 3.1.6      |
| [`langid`](https://pypi.org/project/langid/)                                         | 1.1.6      |
| [`lxml_html_clean`](https://pypi.org/project/lxml_html_clean/)                       | 0.4.2      |
| [`mysql-connector-python`](https://pypi.org/project/mysql-connector-python/)         | 9.4.0      |
| [`numpy`](https://pypi.org/project/numpy/)                                           | 2.2.4      |
| [`opencv-python`](https://pypi.org/project/opencv-python/)                           | 4.11.0.86  |
| [`openpyxl`](https://pypi.org/project/openpyxl/)                                     | 3.1.5      |
| [`orjson`](https://pypi.org/project/orjson/)                                         | 3.11.0     |
| [`pandas`](https://pypi.org/project/pandas/)                                         | 2.2.3      |
| [`paramiko`](https://pypi.org/project/paramiko/)                                     | 4.0.0      |
| [`pdf2image`](https://pypi.org/project/pdf2image/)                                   | 1.17.0     |
| [`pillow`](https://pypi.org/project/pillow/)                                         | 11.1.0     |
| [`plotly`](https://pypi.org/project/plotly/)                                         | 5.24.1     |
| [`PyMuPDF`](https://pypi.org/project/PyMuPDF/)                                       | 1.25.5     |
| [`pypdf`](https://pypi.org/project/pypdf/)                                           | 5.4.0      |
| [`pyarrow`](https://pypi.org/project/pyarrow/)                                       | 18.1.0     |
| [`pydantic`](https://pypi.org/project/pydantic/)                                     | 2.11.3     |
| [`python-dateutil`](https://pypi.org/project/python-dateutil/)                       | 2.9.0      |
| [`python-docx`](https://pypi.org/project/python-docx/)                               | 1.1.2      |
| [`python-multipart`](https://pypi.org/project/python-multipart/)                     | 0.0.20     |
| [`python-pptx`](https://pypi.org/project/python-pptx/)                               | 1.0.2      |
| [`pytz`](https://pypi.org/project/pytz/)                                             | 2024.2     |
| [`reportlab`](https://pypi.org/project/reportlab/)                                   | 4.3.1      |
| [`requests`](https://pypi.org/project/requests/)                                     | 2.32.3     |
| [`scikit-learn`](https://pypi.org/project/scikit-learn/)                             | 1.6.1      |
| [`selenium`](https://pypi.org/project/selenium/)                                     | 4.31.0     |
| [`smartsheet-python-sdk`](https://pypi.org/project/smartsheet-python-sdk/)           | 3.0.5      |
| [`snowflake-connector-python`](https://pypi.org/project/snowflake-connector-python/) | 3.15.0     |
| [`tabulate`](https://pypi.org/project/tabulate/)                                     | 0.9.0      |
| [`trafilatura`](https://pypi.org/project/trafilatura/)                               | 2.0.0      |
| [`uvicorn`](https://pypi.org/project/uvicorn/)                                       | 0.38.0     |
| [`weasyprint`](https://pypi.org/project/weasyprint/)                                 | 66.0       |
| [`webdriver-manager`](https://pypi.org/project/webdriver-manager/)                   | 4.0.2      |
| [`watchdog`](https://pypi.org/project/watchdog/)                                     | 3.0.0      |
| [`websockets`](https://pypi.org/project/websockets/)                                 | 15.0.1     |
| [`xhtml2pdf`](https://pypi.org/project/xhtml2pdf/)                                   | 0.2.17     |
| [`xmltodict`](https://pypi.org/project/xmltodict/)                                   | 0.14.2     |
| [`yfinance`](https://pypi.org/project/yfinance/)                                     | 0.2.58     |

<feedback />


# Access secrets in Python code
Source: https://dev.writer.com/agent-builder/python-secrets



Agent Builder provides **Vault**, which is a secure way to store and use secrets in your agents. Use Vault to store sensitive information like API keys, passwords, and other credentials that you don't want to expose in your code. Secrets are available in [blueprint blocks](/agent-builder/secrets#use-a-secret) and within Python code.

<img alt="Vault overview" />

## Create a secret

Secrets are strings stored as key-value pairs. To create a secret, go to the **Vault** tab in the Agent Builder UI and click **+Add a pair**.

The example below creates a secret with the name `WRITER_API_KEY`. When you type the value, it's masked in the UI.

Click **Save** to store the secret.

<img alt="Add a secret" />

You can also delete and update secrets from the **Vault** tab.

## Where secrets are available

Within Python code, Vault is a runtime-only feature that's injected into specific execution contexts, not into the main module scope. It isn't a global variable and is only available in the execution context of the blueprint. This design provides security benefits:

* Secrets are only loaded when needed
* Access is limited to proper execution contexts
* No global exposure of sensitive data

The vault is only available in these specific contexts:

* **Event handlers and blueprint code blocks**: Secrets are injected into event handlers and blueprint code blocks when they run. This includes:
  * Button click handlers
  * Form submission handlers
  * Page load handlers
  * Custom event handlers
* **Blueprint execution environment**: Secrets are injected into the blueprint execution environment when the blueprint runs and can be referenced from any blueprint block.

### Access your Writer API key

Each Agent Builder agent in the online editor has a Writer API key set as an environment variable called `WRITER_API_KEY`. If you want to use this API key with other Writer API calls, you can access it with `os.getenv('WRITER_API_KEY')`. This allows you to access the API key outside of event handlers and blueprint code blocks, and means you don't need to set a new secret in Vault for your API key.

```python theme={null}
import os

api_key = os.getenv('WRITER_API_KEY')
```

## Examples

### Secrets in Python code blocks

You can reference secrets in [Python code blocks](/blueprints/pythoncode) within blueprints using the `vault` object. `vault` is a dictionary that contains all the secrets in your blueprint.

For example, to access a secret called `ACME_API_KEY` and use it in an HTTP request, you would use the following code:

```python theme={null}
headers = {
    "Authorization": f"Bearer {vault['ACME_API_KEY']}"
}
```

### Event handler with vault access

Vault is also provided as an argument to event handlers. Here's an example of an event handler that's triggered when a button is clicked and accesses the vault.

```python theme={null}
def handle_button_click(state, payload, context, session, ui, blueprint_runner, vault):
    # Access vault secrets
    api_key = vault.get('ACME_API_KEY')
    
    # Use the secret
    headers = {"Authorization": f"Bearer {api_key}"} 
```

<feedback />


# Agent Builder Quickstart
Source: https://dev.writer.com/agent-builder/quickstart

Build your first agent in Agent Builder. Create a meeting notes summarizer with a visual interface and blueprint in this step-by-step tutorial.

<Warning>
  Agent Builder is in beta. Some features are still in development and are subject to change.
</Warning>

This quickstart walks through building a new agent that summarizes meeting notes in different formats for various stakeholders.

<CardGroup>
  <Card title="Interface">
    <img alt="Agent Builder interface showing text input for meeting notes and dropdown for summary format" />
  </Card>

  <Card title="Blueprint">
    <img alt="Agent Builder blueprint with blocks for processing meeting notes and generating summaries" />
  </Card>
</CardGroup>

This quickstart covers how to:

* [Create a new Agent Builder project](#create-a-new-agent-builder-project)
* [Clear the demo agent that's automatically created](#clear-the-demo-agent)
* [Build the agent's UI to accept meeting notes and select a summary format](#build-the-ui)
* [Build the agent's blueprint to summarize the meeting notes and display the result in the UI](#build-the-blueprint)
* [Preview the agent](#preview-the-agent)
* [Deploy the agent](#deploy-the-agent)

<Tip>
  If you are unfamiliar with Agent Builder:

  * Review the [Agent Builder overview](/agent-builder/overview) to learn more about the different components of an agent.
  * Follow the [Demo agent walkthrough](/agent-builder/demo-agent) to see how to modify an existing agent.
</Tip>

## Choose your development approach

This quickstart shows the cloud development workflow. If you prefer to develop locally with your editor of choice, see [Develop agents locally](/agent-builder/local-development) for the local development workflow.

You can follow the same steps here to build and deploy the quickstart agent locally once you've initialized a local project.

## Create a new Agent Builder project

To create an agent with Agent Builder, log in to [AI Studio](https://app.writer.com/aistudio) and follow these steps:

<Steps>
  <Step title="Click the Build an agent button">
    Click the **Build an agent** button in the top right corner of the page.

    <img alt="Build an agent button" />
  </Step>

  <Step title="Create a new agent">
    In the modal that appears, select **New Agent** to create a new agent from scratch.

    <Tip>
      For common use cases, prebuilt agent templates are available. These include both general-purpose and industry-specific options, all customizable to your specific requirements.
    </Tip>

    <img alt="Agent creation options" />

    Once the agent is created, a new tab will open in your browser with the Agent Builder interface.
  </Step>

  <Step title="Return to the Agent Builder interface">
    You can get to the Agent Builder interface any time by going to the [AI Studio homepage](https://app.writer.com/aistudio) and selecting the agent you created.

    You'll see the Configure Deployment page when you select the agent. To get to the edit interface, click the **Edit** button in the top right corner.

    <img alt="Edit agent" />
  </Step>
</Steps>

## Clear the demo agent

When you create a new Agent Builder project, a demo agent is automatically created for you. You can remove the demo agent's components and blueprints to start building your own agent.

### Clear the UI

<Steps>
  <Step title="Delete the top-level Page component from the Component Tree">
    Navigate to the **Interface** view. Then select the **Interface Layers** section in the left sidebar.

    Click the top **Page** component from the UI component tree.

    <img alt="" />

    Then click the three vertical dots on the Page's settings menu to find the **Delete** option.

    <img alt="" />
  </Step>

  <Step title="Add a new Page">
    Your Component Tree should now contain a single **Root** component.

    Click **Add page** at the bottom of the component tree to create a new blank page.

    <img alt="" />
  </Step>
</Steps>

### Clear the blueprint

<Steps>
  <Step title="Delete the first component under Blueprints Root">
    Navigate to the **Blueprints** view. Then select the **Blueprints Layers** section in the left sidebar.

    Click the first component under **Blueprints Root**. Here, the component is a blueprint called `BUTTON@CLICK_1`.

    <img alt="" />

    Then click the three vertical dots on the blueprint's settings menu to find the **Delete** option.

    <img alt="" />
  </Step>

  <Step title="Add a new Blueprint">
    Your blueprint's component tree should now be empty.

    Click **Add blueprint** at the bottom of the component tree to create a new blank blueprint.

    <img alt="" />
  </Step>
</Steps>

## Build the UI

Now that you have an empty page, build the UI for the new agent. This agent has the following components:

* A **Text area** for users to paste their meeting notes
* A **Select input** for users to select the summary format they want
* A **Button** to start the summarization process
* A **Message** component to display a loading message while summarization is in progress
* A **Text** component to display the summary

Navigate to the **Interface** view to build the UI.

### Add the Text area Input

The [**Text area Input** component](/components/textareainput) is where users can paste their meeting notes.

<Steps>
  <Step title="Add the Text area Input component to the canvas">
    Make sure you are in the **Interface** view. Then open the **Add block** section in the left sidebar and search for the **Text area Input** component in the component library.

    Click and drag the **Text area Input** component onto the canvas.
  </Step>

  <Step title="Edit the Text area Input component settings">
    Click the **Text area Input** component you just added to edit its settings. Update the following settings:

    * **Label**: `Meeting notes`
    * **Placeholder**: `Paste your meeting notes here.`
    * **Link variable** under **Binding**: `meeting_notes`. This allows the blueprint to access the meeting notes content whenever the user updates it.

    <Tip>
      The "link variable" is stored in the agent's state. The state is a set of variables that both the blueprint and the UI can access and update. You can learn more about the agent's state in [Agent state](/agent-builder/state).
    </Tip>

    <img alt="" />
  </Step>
</Steps>

### Add the select input

The [**Select input** component](/components/selectinput) is where users can choose what type of summary they want via a dropdown.

<Steps>
  <Step title="Add the select input component to the canvas">
    Search for the **Select input** component in the component library on the left side of the page.

    Click and drag the **Select input** component onto the canvas.
  </Step>

  <Step title="Edit the select input component settings">
    Click the **Select input** component you just added to edit its settings. Update the following settings:

    * **Label**: `Summary format`
    * **Options**: Add the options you want to display in the dropdown. For this example, add the following options, with the key and the value both set to the option name.
      * `Executive brief`
      * `Action items only`
      * `Full summary`
      * `Key decisions`
    * **Link variable** under **Binding**: `summary_format`. This allows the blueprint to access the selected format whenever the user changes it.

      <img alt="" />
  </Step>
</Steps>

### Add the summarize button

Next, add a [**Button** component](/components/button) to the UI. In a later step, you'll connect this button to the blueprint that triggers the summarization process.

<Steps>
  <Step title="Add the button component to the canvas">
    Search for the **Button** component in the block library on the left side of the page. Then click and drag the **Button** component onto the canvas.
  </Step>

  <Step title="Edit the button component settings">
    Click the **Button** component you just added to open its settings. Update the following settings:

    * **Text**: `Generate summary`

      <img alt="" />
  </Step>
</Steps>

### Add the progress message component

The [**Message** component](/components/message) displays messages to users. Here, you use it to display a loading message while summarization is in progress.

<Steps>
  <Step title="Add the message component to the canvas">
    Search for the **Message** component in the block library. Then click and drag the **Message** component onto the canvas.
  </Step>

  <Step title="Edit the message component settings">
    Click the **Message** component you just added to edit its settings. Update the following settings:

    * **Message**: `@{status}`. This reads from the `status` state variable, which the blueprint will set to indicate the progress of the summarization.

      <img alt="" />
  </Step>
</Steps>

### Add the summary text component

The [**Text** component](/components/text) is where the blueprint displays the meeting summary when it's complete.

<Steps>
  <Step title="Add the text component to the canvas">
    Search for the **Text** component in the block library on the left side of the page. Then click and drag the **Text** component onto the canvas.

    <Tip>
      There are multiple components with the name **Text** in them. Make sure you select the **Text** component under the **Content** section of the component library.
    </Tip>
  </Step>

  <Step title="Edit the text component settings">
    Click the **Text** component you just added to edit its settings. Update the following settings:

    * **Text**: `@{summary}`. This displays the value of the `summary` state variable, which the blueprint sets to the meeting summary.
    * **Use Markdown**: Select **Yes**. This allows you to use markdown formatting in the text.

      <img alt="" />
  </Step>
</Steps>

## Build the blueprint

With the UI built, you can now build the blueprint for the new agent.

The blueprint has the following logic:

1. The **UI trigger** block triggers the blueprint when the user clicks the **Generate summary** button in the UI.
2. The **Set state** block sets the `status` state variable to `Summarizing` so that the `Message` component in the UI can display it.
3. The **Text generation** block generates a summary of the meeting notes based on the selected format.
4. The **Set state** block sets the `summary` state variable to the generated summary so that the `Text` component in the UI can display it.
5. The **Set state** block sets the `status` state variable back to an empty string to clear the loading message from the UI.

Navigate to the **Blueprints** view to build the blueprint.

### Add the UI trigger

The [**UI trigger** block](/blueprints/uitrigger) triggers the blueprint when the user clicks the **Generate summary** button in the UI.

<Steps>
  <Step title="Add the UI trigger block to the canvas">
    Make sure you are in the **Blueprints** view. Then open the **Add block** section in the left sidebar and search for the **UI trigger** block in the blueprints toolkit.

    Click and drag the **UI trigger** block onto the canvas.
  </Step>

  <Step title="Edit the UI trigger block settings">
    Click the **UI trigger** block you just added to edit its settings. Update the following settings:

    * **Component ID**: Select the button component (it should show as `Generate summary`)
    * **Event type**: `wf-click`. This triggers the blueprint when the user clicks the **Generate summary** button in the UI.

      <img alt="" />
  </Step>
</Steps>

### Add the set state block for the progress message

The [**Set state** block](/blueprints/setstate) sets the `status` state variable to `Summarizing` so that the `Message` component in the UI can display it.

<Steps>
  <Step title="Add the set state block to the canvas">
    Search for the **Set state** block. Then click and drag the **Set state** block onto the canvas.

    Connect the **Set state** block to the **UI trigger** block by dragging a line from the green dot on the **UI trigger** block to the **Set state** block. This tells the blueprint to execute the **Set state** block after the UI Trigger completes.
  </Step>

  <Step title="Edit the set state block settings">
    Click the **Set state** block you just added to edit its settings. Update the following settings:

    * **Link variable**: `status`
    * **Value**: `% Generating summary`. The `%` symbol indicates that the message should display a dynamic loading symbol.

    The **Message** component in the UI displays the loading message when this block is executed.

    <img alt="" />
  </Step>
</Steps>

At this point, you can preview the agent to see the new behavior. When you click the **Generate summary** button, the `Message` component displays the loading message that you set in the **Set state** block.

<img alt="" />

### Add the text generation block

The [**Text generation** block](/blueprints/textgeneration) generates a summary of the meeting notes based on the selected format using Palmyra LLMs.

<Steps>
  <Step title="Add the text generation block to the canvas">
    Search for the **Text generation** block in the block library. Then click and drag the **Text generation** block onto the canvas.

    Connect the previous **Set state** block to this **Text generation** block. Drag a line from the green dot on the **Set state** block to the **Text generation** block. This tells the blueprint to execute the **Text generation** block after the **Set state** block completes successfully.
  </Step>

  <Step title="Edit the text generation block settings">
    Click the **Text generation** block you just added to edit its settings. Update the following settings:

    * **Prompt**: Enter the following prompt. This prompt references the `meeting_notes` and `summary_format` state variables, which you set up in the UI. It instructs the agent to summarize the meeting notes and defines the different formats.

    ```
    Summarize these meeting notes based on the requested format:

    Meeting Notes: @{meeting_notes}
    Format: @{summary_format}

    Guidelines for each format:
    - Executive brief: Create a concise 2-3 sentence overview focusing on high-level outcomes and decisions for leadership
    - Action items only: List only the specific action items, who is responsible, and deadlines
    - Full summary: Provide a comprehensive summary including discussion points, decisions, action items, and next steps
    - Key decisions: Focus specifically on decisions made during the meeting and their implications

    Format the output appropriately for the selected format type.
    ```

    <img alt="" />
  </Step>
</Steps>

### Add the set state block for the summary result

This [**Set state** block](/blueprints/setstate) sets the `summary` state variable to the generated summary so that the Text component in the UI can display it.

<Steps>
  <Step title="Add the set state block to the canvas">
    Search for the **Set state** block in the block library. Then click and drag the **Set state** block onto the canvas.

    Connect the new **Set state** block to the **Text generation** block. Drag a line from the green dot on the **Text generation** block to the new **Set state** block. This tells the blueprint to execute the new block after the **Text generation** block completes successfully.
  </Step>

  <Step title="Edit the set state block settings">
    Click the **Set state** block you just added to edit its settings. Update the following settings:

    * **Link variable**: `summary`
    * **Value**: `@{result}`. The `result` environment variable is the output of the previous **Text generation** block.

    <Tip>
      The `@{result}` syntax is how you reference the output of previous blocks. Learn more about what data and variables are available to reference in [Using data from previous blocks](/agent-builder/execution-environment).
    </Tip>

    <img alt="" />
  </Step>
</Steps>

### Add the set state block to clear the status message

This [**Set state** block](/blueprints/setstate) clears the `status` state variable to remove the status message from the UI.

<Steps>
  <Step title="Add the set state block to the canvas">
    Search for the **Set state** block in the block library. Then click and drag the **Set state** block onto the canvas.

    Connect the new **Set state** block to the previous **Set state** block. Drag a line from the green dot on the previous **Set state** block to the new **Set state** block. This tells the blueprint to execute the new block after the previous block completes successfully.
  </Step>

  <Step title="Edit the set state block settings">
    Click the **Set state** block you just added to edit its settings. Update the following settings:

    * **Link variable**: `status`
    * **Value**: Leave the **Value** blank. This clears the `status` state variable.

      <img alt="" />
  </Step>
</Steps>

### Data connection patterns

Notice how the blueprint connects blocks and passes data using variables:

* `@{meeting_notes}` and `@{summary_format}`: the blueprint blocks read from UI inputs via state
* `@{result}`: the blueprint blocks read the output from the Text generation block
* `@{summary}`: the blueprint blocks write the summary to the state, which the UI displays
* `@{status}`: the blueprint blocks set progress messages that the UI displays

This pattern of using `@{variable_name}` is how all Agent Builder components communicate.

## Preview the agent

You've now built the UI and blueprint for the new agent. Go to the **Preview** view to test the agent.

Paste meeting notes into the **Meeting notes** text area and select a summary format from the **Summary format** select input. Then click the **Generate summary** button to see the agent create a summary in your selected format.

Here's a sample transcript that you can use to test if you don't have your own meeting notes:

<Expandable title="Sample transcript">
  ```text theme={null}
  1
  00:00:00,000 --> 00:00:03,500
  Sarah: Alright everyone, let's start with our Q1 campaign performance.

  2
  00:00:03,500 --> 00:00:07,000
  Sarah: Mike, how are we tracking on the content calendar?

  3
  00:00:07,000 --> 00:00:12,500
  Mike: We're slightly behind on blog posts - only published 8 out of 12 planned for January.

  4
  00:00:12,500 --> 00:00:18,000
  Mike: The SEO-focused pieces are taking longer than expected because we're doing more keyword research.

  5
  00:00:18,000 --> 00:00:22,500
  Mike: The good news is organic traffic is up 18% month-over-month.

  6
  00:00:22,500 --> 00:00:28,000
  Mike: I think we can catch up by end of month if we get approval for two freelance writers at $150 per article.

  7
  00:00:28,000 --> 00:00:32,500
  Sarah: Approved. That's within our content budget. Alex, what's the budget looking like for paid campaigns?

  8
  00:00:32,500 --> 00:00:37,000
  Alex: We've spent 60% of Q1 budget already, but conversion rates are 23% higher than last quarter.

  9
  00:00:37,000 --> 00:00:42,500
  Alex: The LinkedIn campaign is performing especially well for enterprise leads.

  10
  00:00:42,500 --> 00:00:47,000
  Alex: We're seeing a 4.2% CTR compared to 2.8% industry average.

  11
  00:00:47,000 --> 00:00:53,500
  Alex: I'd like to shift $15K from the underperforming Google Display campaign to LinkedIn and maybe test some YouTube ads.

  12
  00:00:53,500 --> 00:00:57,000
  Sarah: Let's do the LinkedIn shift but hold off on YouTube until we see February numbers.

  13
  00:00:57,000 --> 00:01:02,500
  Sarah: We need to be more conservative with spend until we understand the pricing impact.

  14
  00:01:02,500 --> 00:01:06,000
  Sarah: Lisa, any updates on the product launch messaging?

  15
  00:01:06,000 --> 00:01:10,500
  Lisa: The beta feedback is mostly positive. Users love the new dashboard interface and automation features.

  16
  00:01:10,500 --> 00:01:16,000
  Lisa: Main concern is pricing - 40% of beta users said our current model is too high for small businesses.

  17
  00:01:16,000 --> 00:01:20,500
  Lisa: They're comparing us to competitors who start at $39/month.

  18
  00:01:20,500 --> 00:01:25,000
  Lisa: I recommend we create a basic tier at $29/month instead of starting at $79.

  19
  00:01:25,000 --> 00:01:30,500
  Lisa: All marketing materials are ready but we'll need to update pricing pages, sales decks, and comparison charts.

  20
  00:01:30,500 --> 00:01:35,000
  Sarah: That's a significant pricing change that could impact our revenue projections by 30-40%.

  21
  00:01:35,000 --> 00:01:39,500
  Sarah: I need to discuss with leadership before we commit.

  22
  00:01:39,500 --> 00:01:45,000
  Sarah: Can you prepare a comprehensive business case by Friday? Include competitive analysis and revenue projections.

  23
  00:01:45,000 --> 00:01:49,500
  Lisa: Absolutely. I'll also coordinate with the sales team to get their input on customer feedback.

  24
  00:01:49,500 --> 00:01:53,000
  Mike: Quick question - should we hold off on publishing the pricing comparison blog post?

  25
  00:01:53,000 --> 00:01:57,500
  Sarah: Good catch. Yes, hold that one back. Focus on the feature-focused content instead.

  26
  00:01:57,500 --> 00:02:02,000
  Alex: Should we pause the Google Display campaign completely or just reduce spend?

  27
  00:02:02,000 --> 00:02:07,500
  Alex: It's eating budget but only generating 12 leads per month at $89 cost per lead.

  28
  00:02:07,500 --> 00:02:12,000
  Sarah: Reduce to minimum viable spend for now - maybe $2K monthly.

  29
  00:02:12,000 --> 00:02:17,500
  Sarah: We might be able to optimize it later once we have more conversion data.

  30
  00:02:17,500 --> 00:02:22,000
  Alex: Got it. I'll also run some A/B tests on our LinkedIn ad creative while we're scaling that up.

  31
  00:02:22,000 --> 00:02:26,500
  Sarah: Let me summarize our action items: Mike to hire freelancers and catch up on content calendar.

  32
  00:02:26,500 --> 00:02:31,000
  Sarah: Alex to reallocate $15K from Display to LinkedIn and reduce Display to $2K monthly.

  33
  00:02:31,000 --> 00:02:36,500
  Sarah: Lisa to prepare pricing analysis by Friday. I'll schedule leadership review for next Tuesday.

  34
  00:02:36,500 --> 00:02:40,000
  Lisa: Should I invite sales leadership to that pricing review?

  35
  00:02:40,000 --> 00:02:44,500
  Sarah: Yes, definitely include Tom from sales. His team will need to adjust their pitch if we change pricing.

  36
  00:02:44,500 --> 00:02:49,000
  Mike: If pricing changes, how much time do we need to update all marketing materials?

  37
  00:02:49,000 --> 00:02:54,500
  Lisa: About two weeks if we prioritize the sales decks and website first. The brochures can follow.

  38
  00:02:54,500 --> 00:02:59,000
  Sarah: That might push our launch timeline back slightly, but better to get pricing right.

  39
  00:02:59,000 --> 00:03:02,500
  Alex: All good on my end. I'll send the campaign performance report by tomorrow.

  40
  00:03:02,500 --> 00:03:05,000
  Sarah: Perfect. Thanks everyone. See you next week.
  ```
</Expandable>

Try different summary formats with the same meeting notes to see how the output changes based on your selection. You can also experiment with leaving the summary format blank to see how the agent handles it.

If you encounter any issues, refer to the [Troubleshooting](/agent-builder/troubleshooting) guide for debugging information.

## Deploy the agent

When you're ready to deploy the agent, click **Configure deployment** in the top right corner of the Agent Builder interface. You can also access the deployment configuration by going to the [AI Studio homepage](https://app.writer.com/aistudio) and selecting the agent you created.

If the agent isn't deployed, you see a toggle bar that says **Draft**.

<img alt="" />

To deploy the agent, toggle the bar. Writer deploys the agent to the Writer cloud, which takes a moment to complete.

When the deployment is complete, the toggle bar shows **Deployed**. It also shows a list of the teams in your organization, which you can use to grant access to the agent. You must select at least one team before you can view the URL for the deployed agent.

<img alt="" />

You can also choose to deploy the agent to the Writer Agent Library for the teams you've granted access to. These teams can see the agent you've created in the main [Ask Writer app](https://app.writer.com).

To help teams find the agent, select **Edit the agent guide**, where you can add a description and other information to help teams use the agent.

<img alt="" />

## Agentic enhancements

This tutorial shows outcome-driven design in action: you define what kind of summary you want, and the agent adapts. To make this agent even more versatile, consider adding:

* [Send summaries to Slack](/agent-builder/invoice-processing): Automatically post to relevant channels
* [HTTP request blocks](/blueprints/httprequest) and [tool calling](/agent-builder/tool-calling) to integrate with third party APIs to perform actions like:
  * **Emailing participants**: Send summaries via your email API such as SendGrid or Mailgun
  * **Calendar integrations**: Schedule follow-up meetings and block time for tasks
  * **CRM updates**: Log meeting outcomes in your customer database
  * **Task automation**: Create Jira tickets or Asana tasks for each action item

These integrations transform a summarizer into a true meeting assistant that takes action on your behalf.

## Next steps

* Learn how to [add file inputs and parse PDFs](/agent-builder/summarize-pdfs)
* Learn how to [style the agent's UI](/agent-builder/component-styles)
* Learn how to [send messages to Slack](/agent-builder/invoice-processing)
* Add [tool calling for intelligent integrations with external tools](/agent-builder/tool-calling)

<feedback />


# Build dynamic UI components with Repeaters
Source: https://dev.writer.com/agent-builder/repeater

Display dynamic lists of data in Agent Builder UI with Repeaters. Create repeating components for lists, dictionaries, and nested data structures.

The [**Repeater** component](/components/repeater) in Agent Builder allows you to display dynamic lists of data in your UI. It's similar to the [For-each loop block](/blueprints/for-eachloop) in blueprints, but for UI elements.

This document provides an overview of Repeaters and how to use them in your Agent Builder agents with two examples.

## Overview

A Repeater takes a list or dictionary and creates a copy of its child components for each item in that data. Inside the Repeater, you can access:

* `@{item}`: the current item's value
* `@{itemId}`: the current item's index, for arrays, or key, for dictionaries

Below is an example interface with a Repeater that displays a list of recipes based on user input. The Repeater is used to display each recipe in a tabbed layout and also to display the ingredients and steps for each recipe as nested text Repeaters.

<img alt="Repeater demo that displays a list of recipes based on user input" />

See the [tutorial](#tutorial%3A-build-a-recipe-generator-with-repeaters) below for more details about building this recipe generator.

### When to use Repeaters

Repeaters are useful for displaying:

* Lists of files, tasks, or messages
* Search results or product catalogs
* User-generated content like comments or reviews
* Data from APIs or structured output
* Any collection where you don't know the exact number of items

## Example: Display a list of items

This example starts with a small use case to dynamically display whatever a user selects from a select input.

<img alt="Repeater example" />

To start, you should have a new Agent Builder agent and clear the demo agent so you can start from scratch. See the instructions for [clearing the demo agent](/agent-builder/quickstart#clear-the-demo-agent) in the Agent Builder quickstart.

### Set up the select input

The select input allows the user to select multiple items from a list. You'll use it to select the items to display in the Repeater.

<Steps>
  <Step title="Add the select input to the agent">
    First, add a **Select input** block to the agent's interface. This is the input that will be used to select the items to display in the Repeater.

    Update the following settings:

    * **Label**: `Items`
    * Under **Options**, add a few key-value pairs as options for the dropdown. For example:
      * Key: Apple, Value: Apple
      * Key: Berry, Value: Berry
      * Key: Cherry, Value: Cherry
    * **Link variable** under **Binding**: `list`

    <Note>
      When you define the options for the select input, the values are what the user sees in the select dropdown, and the keys are what will be stored in the state variable.
    </Note>
  </Step>

  <Step title="Add a Heading component">
    Add a **Heading** component to the agent's interface. This separates the select input from the Repeater.

    Under **Text**, update the text to `You selected:`. Next, you'll add the Repeater below this heading.

    <img alt="Heading component" />
  </Step>
</Steps>

### Add the Repeater

The Repeater displays a list of items dynamically. You'll use it to display the items selected from the select input.

<Steps>
  <Step title="Add a Repeater component">
    Add a **Repeater** component to your page. Update the following settings:

    * **Repeater object**: `@{list}`. This is the list of items to display in the Repeater.

    The **Key variable name** and the **Value variable name** default to `itemId` and `item`. These are the variable names you use to access the current item's index and value within the Repeater. You can change them to anything you want. In this example, you'll use the default names.

    <img alt="Repeater component" />
  </Step>

  <Step title="Add a Text component inside the Repeater">
    Drag a **Text** component inside the Repeater.

    Inside the text component, set the text to ` @{item}` to display the value of the current item in the Repeater.

    When the `list` state variable is empty, the text displays placeholder text. When the user selects items from the multiselect input, the text displays the selected items.

    <img alt="Text component" />
  </Step>

  <Step title="Update the Repeater's visibility">
    Once you add components to a Repeater, you can no longer see the parent Repeater component in the agent's interface view. You can find and edit the invisible component from the **Interface Layers** tab.

    <img alt="Repeater component" />

    Navigate to the **Interface Layers** sidebar within the agent's **Interface** tab. Then, click the Repeater component to open its configuration menu.

    Under **Visibility** in the Repeater's configuration menu, select **Custom**. Set the condition to `list`. This ensures the Repeater is only visible when the user has selected at least one item from the select input and the `list` state variable isn't empty.

    <img alt="Repeater visibility" />

    <Tip>
      Once you set a component's visibility to become hidden, you can no longer see it or its children within the agent's interface view. You can find and edit the invisible component from the **Interface Layers** tab.
    </Tip>
  </Step>

  <Step title="Preview the Repeater">
    Navigate to the **Preview** tab to see the Repeater in action.

    Select a few items from the select input to see the Repeater display the corresponding items.

    <img alt="Repeater preview" />
  </Step>
</Steps>

## Tutorial: Build a recipe generator with Repeaters

This tutorial uses multiple Repeaters to display a list of recipes based on user input.

<img alt="Repeater demo that displays a list of recipes based on user input" />

The recipe generator:

* Accepts a list of dish names from the user
* Generates complete recipes in the blueprint using AI structured output
* Displays each recipe in an organized, tabbed layout using nested Repeaters

### Part 1: Set up a minimal UI and the blueprint

First, you'll set up a minimal UI and the blueprint. This UI won't use any Repeaters yet, but it will be the foundation for the recipe generator.

#### Create the input interface

<Steps>
  <Step title="Add a Text Input">
    Add a **Text Input** to your page. Update the following settings:

    * **Label**: `Recipes`
    * **Placeholder text**: `Enter dish names (e.g., "Pizza, Chocolate Chip Cookies, Tacos")`
    * **Link variable** under **Binding**: `request`
  </Step>

  <Step title="Add a Button to trigger the blueprint">
    Add a **Button** to your page. Update the following settings:

    * **Label**: `Get recipes`
  </Step>
</Steps>

<img alt="Minimal UI" />

#### Build the blueprint

The blueprint contains three blocks:

* **UI Trigger** to trigger the blueprint when a user clicks the **Get recipes** button
* **Structured Output** block to generate the recipes and provide structured output that the Repeaters can use
* **Set State** block to store the recipes in the state variable `recipes`

<Steps>
  <Step title="Add a UI Trigger block">
    Add a **UI Trigger** block to your blueprint. Update the following settings:

    * **Component Id**: Select the **Get recipes** button
    * **Action**: `wf-click`
  </Step>

  <Step title="Add a Structured Output block">
    Add a **Structured Output** block to your blueprint. Update the following settings:

    * **Prompt**:
      ```
      You are a helpful cooking assistant. The user will provide you with a list of dish names. For each dish mentioned, create a complete recipe with the name, ingredients list, and step-by-step cooking instructions.

      Guidelines:
      - Provide practical, easy-to-follow recipes suitable for home cooking
      - Include specific measurements for ingredients (cups, tablespoons, etc.)
      - Write clear, numbered steps that a beginner could follow
      - Keep recipes simple but complete
      - If a dish name is vague, choose a popular/classic version of that dish

      For each dish in the user's input, generate a separate recipe object with:
      - name: The specific recipe name
      - ingredients: A detailed list of all ingredients with measurements
      - steps: Clear, sequential cooking instructions

      Example input: "Pizza, Chocolate Chip Cookies"
      This should generate recipes for both pizza and chocolate chip cookies.

      Process all dishes mentioned in the user's input and provide complete recipes for each one.

      User input: @{request}
      ```

    * **JSON Schema**: The JSON schema below defines the structure of the recipes as a list of recipe objects. Each recipe object has a name, ingredients, and steps. The Repeaters will use this schema to display the recipes.
      ```json theme={null}
      {
        "type": "array",
        "description": "Array of recipe objects",
        "items": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string",
              "description": "The name of the recipe"
            },
            "ingredients": {
              "type": "array",
              "description": "List of ingredients needed for the recipe",
              "items": {
                "type": "string"
              }
            },
            "steps": {
              "type": "array",
              "description": "Step-by-step cooking instructions",
              "items": {
                "type": "string"
              }
            }
          },
          "required": ["name", "ingredients", "steps"]
        }
      }
      ```
  </Step>

  <Step title="Add a Set State block">
    Add a **Set State** block to your blueprint. Update the following settings:

    * **State variable**: `recipes`
    * **Value**: `@{result}`
  </Step>
</Steps>

The complete blueprint should look like this:

<img alt="Complete blueprint" />

#### Test the blueprint

Now you can test that the blueprint works before you add the Repeaters.

Navigate to the **Preview** tab to test the blueprint.

Enter something like "Pizza, Chocolate Chip Cookies" in the text input and click the **Get recipes** button.

You should see the `request` and the structured output in the `result` state variables in your state before moving to the next step.

<img alt="State variables" />

### Part 2: Add the main recipe Repeater

Navigate back to the **Interface** tab to continue building the recipe generator's UI.

<Steps>
  <Step title="Add a Repeater component">
    Add a **Repeater** component to your page. Update the following settings:

    * **Repeater object**: `@{recipes}`. This is the list of recipes you stored in the `recipes` state variable.
  </Step>

  <Step title="Add a Heading component inside the Repeater">
    Add a **Heading** component inside the Repeater. Update the following settings:

    * **Text**: `@{item.name}`

    This displays the name of each recipe as a heading.
  </Step>
</Steps>

If you preview the agent now, you should see a heading for each recipe name.

<img alt="Repeater preview" />

At this point, when there are no recipes, the Repeater still displays placeholder text. At the end of the tutorial you'll update the Repeater to be invisible when there are no recipes.

### Part 3: Add nested Repeaters for ingredients and steps

Now you'll add nested Repeaters for ingredients and steps. You'll use a **Tab Container** to display the ingredients and steps in separate tabs.

<Steps>
  <Step title="Add a Tab Container inside the main Repeater">
    Add a **Tab Container** inside the Repeater by dragging it into the space that contains the heading. You can verify that the tab container is inside the Repeater by checking the **Interface Layers** sidebar.

    <img alt="Tab Container inside the Repeater" />
  </Step>

  <Step title="Create the first tab">
    Add a **Tab** component to the tab container. Update the following settings:

    * **Tab name**: "Ingredients"

    Then drag a **Repeater** inside the tab. Update the following settings:

    * **Repeater object**: `@{item.ingredients}`

      <img alt="Ingredients tab" />
  </Step>

  <Step title="Add a Text component inside the first tab's Repeater">
    Add a **Text** component inside the first tab's Repeater. Update the following settings:

    * **Text**: `- @{item}`

      <img alt="Ingredients tab" />
  </Step>

  <Step title="Create the second tab with a Repeater">
    Add a second **Tab** component to the tab container. Update the following settings:

    * **Tab name**: "Instructions"

    Then drag a **Repeater** inside the tab. Update the following settings:

    * **Repeater object**: `@{item.steps}`

      <img alt="Ingredients tab" />
  </Step>

  <Step title="Add a Text component inside the second tab's Repeater">
    Add a **Text** component inside the second tab's Repeater. Update the following settings:

    * **Text**: `- @{item}`

      <img alt="Instructions tab" />
  </Step>

  <Step title="Update the Repeater's visibility">
    From the **Interface Layers** sidebar, click the first Repeater component, which contains the headings and tab components. Under **Visibility** in the main Repeater's configuration menu, select **Custom**. Set the condition to `recipes`. This ensures the Repeater is only visible when the user has selected at least one recipe.

    <img alt="Repeater visibility" />
  </Step>
</Steps>

If you preview the agent now, you should see the ingredients and steps for each recipe in separate tabs. You also shouldn't see the Repeater if there are no recipes.

### Understanding nested Repeaters

In this tutorial, you used three different Repeaters:

1. **Main Repeater**: iterates over the recipes array (`@{recipes}`)
2. **Ingredients Repeater**: iterates over each recipe's ingredients array (`@{item.ingredients}`). This Repeater is nested inside the main Repeater.
3. **Steps Repeater**: iterates over each recipe's steps array (`@{item.steps}`)

Each Repeater creates its own `@{item}` context, so the inner Repeaters access individual ingredients and steps as `@{item}`, while the outer Repeater's `@{item}` refers to the entire recipe object.

### Next steps

Try extending this tutorial by:

* Adding cooking time and difficulty level to the recipe schema
* Styling the recipe cards with colors and spacing
* Adding images or icons to make the recipes more visual
* Creating filters to show only certain types of recipes


# Store secrets with Vault
Source: https://dev.writer.com/agent-builder/secrets

Store API keys, passwords, and credentials securely with Vault in Agent Builder. Reference secrets in blueprints and Python code.

Agent Builder provides **Vault**, which is a secure way to store and use secrets in your agents. Use Vault to store sensitive information like API keys, passwords, and other credentials.

You can reference secrets in any block in your blueprint and [within Python code](/agent-builder/python-secrets).

<img alt="Vault overview" />

## Create a secret

Secrets are strings stored as key-value pairs. To create a secret, go to the **Vault** tab in the Agent Builder UI and click **+Add a pair**.

The example below creates a secret with the name `WRITER_API_KEY`. When you type the value, it's masked in the UI.

Click **Save** to store the secret.

<img alt="" />

You can also delete and update secrets from the **Vault** tab.

## Use a secret

To use a secret, reference it in a block in your blueprint with the prefix `@{vault}`. For example, to use the `WRITER_API_KEY` secret, you would use it as `@{vault.WRITER_API_KEY}`.

The example below shows adding an authorization header to an HTTP request block using the `WRITER_API_KEY` secret.

<img alt="" />

When you reference a secret, it's automatically masked in the UI.

<img alt="" />

## Use a secret in Python code

You can access secrets in Python code blocks within blueprints using the `vault` object. `vault` is a dictionary that contains all the secrets in your blueprint.

<Warning>
  Vault secrets are only available in **blueprint code blocks** and **event handlers**. They are not available in `main.py` or other Python modules at the global scope. This is by design for security reasons. If you need to access secrets in `main.py`, use environment variables like `os.getenv('WRITER_API_KEY')` instead.
</Warning>

For example, to access a secret called `API_KEY` and use it in an HTTP request, you would use the following code:

```python theme={null}
headers = {
    "Authorization": f"Bearer {vault['API_KEY']}"
}
```

Learn more about [accessing secrets in Python code](/agent-builder/python-secrets).

## Next steps

Now that you've learned how to store and use secrets in Vault, learn how to [make authenticated API calls using HTTP request blocks](/blueprints/httprequest) with your stored credentials.

**More resources**

* [Access secrets in Python code](/agent-builder/python-secrets) - Learn how to use vault secrets in event handlers and Python code blocks
* [Python code blocks](/blueprints/pythoncode) - Add custom Python logic to your blueprints
* [Local development](/agent-builder/local-development) - Set up local development with environment variables

<feedback />


# Manage agent state
Source: https://dev.writer.com/agent-builder/state



The agent's state is a core component of the agent's behavior. It contains values that the UI, blueprint, and custom Python code can access and update. The state acts as a shared memory for each part of the agent.

## Agent state explained

Think of an agent's "state" like a memory or a notebook that stores important information. Just like how you might jot down notes to remember things, an agent uses its state to keep track of various values or data that it needs to perform its tasks.

Imagine you're building a chatbot using Writer's Agent Builder. The chatbot needs to remember the user's name, so it can greet them by name later. In this case, the agent stores the user's name in its state as a variable. The chatbot can access this information and use it to personalize its responses.

The state is like a shared notebook that different parts of the agent can read from and write to. This allows the agent to keep track of its progress, remember important details, and make decisions based on the information stored in its state.

<img alt="Agent state explained" />

<Note>
  Agent state is session-based, temporary, and visible in the UI.

  * For persistent, non-sensitive data: use the [**Key-Value Storage** block](/blueprints/key-valuestorage) to store data in the cloud.
  * For sensitive data: use [**Vault**](/agent-builder/secrets) to store sensitive information like API keys, passwords, and credentials.
</Note>

## Inspect the state

You can view the agent's state from any view by clicking the **State explorer** icon in the top right of the page. This is helpful for debugging and understanding how the agent is working.

<img alt="State explorer icon" />

## Set state variables

You can set and update state variables in two ways:

* In a UI input element
* Using the **Set state** block in the blueprint
* Using custom Python code

### Set state in a UI input element

You can set state variables in any input block in the UI under the **Binding** section of the UI element's properties.

When a user enters a value in the input element, the value is automatically set as a state variable with the name you provide in the **Binding** section. Other parts of the agent can access this value.

<img alt="Set state in a UI input element" />

### Set state block

The [**Set state** block](/blueprints/setstate) allows you to set a state variable within a blueprint. Provide the name of the state variable and the value you want to set. When the block runs, the state variable is created or updated with the provided value.

<img alt="Set state block" />

You can set state variables as `text` or `JSON`. Under the **Value type** dropdown, select the type of value you want to set. In the **Value** field, enter the value you want to set.

If you set the value as `text`, the value of the state variable is stored as a string. If you set the value as `JSON`, the value of the state variable is stored as a JSON object.

See how to [access nested state variables that are set as JSON](#nested-state-variables) below.

### Set state with custom Python

You can also set state variables with custom Python code.

To initialize the state, use the `wf.init_state` function:

```python theme={null}
import writer as wf

wf.init_state({
    "counter": 0
})
```

To update the state, define a function that takes the state as an argument and updates the state. The state is similar to a Python dictionary, so you can update it by accessing the key and assigning a new value.

```python theme={null}
def increment(state):
    state["counter"] += 1
```

#### Private state variables

You can create private state variables with custom Python code. Private state variables are not visible to the agent's UI and are only accessible via code.

To create a private state variable, prefix the variable with an underscore (`_`).

```python theme={null}
wf.init_state({
    "_user_name": "John Doe"
})
```

## Access state variables

You can access state variables in the agent's blueprint, UI, and custom Python code.

### Access state variables in a UI element

You can access state variables in a UI element by using the `@` syntax.

<img alt="Access state variables in a UI element" />

### Access state variables in a blueprint

Access state variables in a blueprint by using the `@` syntax.

<img alt="Access state variables in a blueprint" />

### Access state variables in custom Python

You can access state variables in custom Python code by referencing the `state` variable. The `state` variable functions like a Python dictionary, so you can access and update state variables by referencing the key.

Pass the `state` variable to the function when you call it.

```python theme={null}
def increment(state):
    state["counter"] += 1
```

## Nested state variables

Agent Builder allows you to create and access nested state variables. This is useful when you need to store complex data that requires multiple levels of organization.

### Set nested state variables

You can set nested state variables in the **Set state** block using `JSON` as the value type and a JSON object as the value.

You can also set nested state variables in custom Python code.

### Access nested state variables

In the UI or a blueprint, use `.` to access the different levels of nested state variables. For example, consider a state variable with the following structure:

```json theme={null}
"tasks": [
    {
        "title": "Write a blog post"
    }
]
```

<img alt="Nested state variables" />

To access the `title` of the first task in a blueprint or UI block, use the following syntax:

```
@{tasks.0.title}
```

<img alt="Accessing nested state variables in a UI element" />

<feedback />


# Upload, parse, and summarize PDFs
Source: https://dev.writer.com/agent-builder/summarize-pdfs

Build a PDF research paper summarizer with Agent Builder. Upload PDFs, parse content, and generate summaries for specific personas.

This tutorial follows a similar format to the [Agent Builder Quickstart](/agent-builder/quickstart) use case of summarizing text; in this case, it adds the ability to upload and parse PDFs with Agent Builder and summarize the content.

This tutorial use case is a research paper summarizer that summarizes papers for a specific persona.

If you haven't completed the Agent Builder Quickstart, you may want to do that first to familiarize yourself with the mechanics of Agent Builder.

## Build the UI

The UI for the agent contains:

* A file input block
* A select input to choose the persona to summarize the file for
* A button to start the summarization process
* A message block to indicate that the file is in progress
* A text block to display the results

<Steps>
  <Step title="Add a File Input block">
    First, add a File Input block to the canvas. In the block's configuration panel, update the following fields:

    * **Label**: `Research paper PDF`
    * **Allowed file types**: `.pdf`
    * **Allow multiple files**: `no`

      <img alt="File Input block" />
  </Step>

  <Step title="Add a select input to choose the persona">
    Next, add a **Select input** block to the canvas, which creates a dropdown. In the block's configuration panel, update the following fields:

    * **Label**: `Persona`
    * **Options**: add the personas you want to summarize the file for. This example uses the personas `Sales`, `Marketing`, `Product`, and `Engineering`.
    * **Link variable** under **Binding**: `persona`. You use this variable name in the blueprint when asking the text generation block to generate a summary for the selected persona.

      <img alt="Persona dropdown" />
  </Step>

  <Step title="Add a button to start the summarization process">
    Add a **Button** block to the canvas. In the block's configuration panel, update the following fields:

    * **Text**: `Summarize`

      <img alt="Start button" />
  </Step>

  <Step title="Add a message block to indicate that the file is in progress">
    Add a **Message** block to the canvas, which will display a message to the user indicating that the summarization is in progress. In the block's configuration panel, update the following fields:

    * **Message**: `@{status}`. This will display the status message from the agent's state. You use this variable name in the blueprint to update the status message in the UI.

      <img alt="In progress message" />
  </Step>

  <Step title="Add a text block to display the results">
    Finally, add a **Text** block to the canvas, which will display the summary results. In the block's configuration panel, update the following fields:

    * **Text**: `@{summary}`. This will display the summary results from the agent's state. You use this  name in the blueprint to display the summary results in the UI.
    * **Use markdown**: `yes`

      <img alt="Results text block" />
  </Step>
</Steps>

## Build the blueprints

The agent has two blueprints:

* [One for uploading a file to Writer cloud](#first-blueprint-upload-file-to-writer-cloud) when a user adds a file to the file input block
* [One for parsing the file and summarizing it](#second-blueprint-parse-and-summarize-the-file) when a user clicks the button to start summarizing

The agent's logic is segmented into two blueprints so that the file uploading can complete before the parsing and summarizing begins.

### First blueprint: Upload file to Writer cloud

Before you can work with the content of a PDF file in a blueprint, you need to upload it to the Writer Cloud with the **Add files to Writer Cloud** block. Once you've uploaded the file, you can access it in the blueprint using the uploaded file's ID and perform operations like parsing the PDF and providing that information to a text generation block.

<Steps>
  <Step title="Add a UI trigger for file input">
    Add a **UI trigger** block to the canvas. This triggers the blueprint when a user adds or changes the file in the file input block.

    In the block's configuration panel, update the following fields:

    * **Alias**: `File upload`. This is optional, but helps to identify the trigger in the blueprint.
    * **Component Id**: Select the `File Input` component from the dropdown of UI blocks
    * **Event type**: `wf-file-change`

      <img alt="File input trigger" />
  </Step>

  <Step title="Add a block to add files to Writer Cloud">
    Add a **Add files to Writer Cloud** block to the canvas. This block uploads the file to Writer Cloud.

    In the block's configuration panel, update the following fields:

    * **Files**: `@{payload}`. This references the payload provided by the preceding UI trigger. The payload contains the file that was uploaded to the file input block.

      <img alt="Add files to Writer Cloud" />
  </Step>

  <Step title="Add the uploaded file information to the state">
    Finally, add a **Set state** block to the canvas. This block takes the results from the **Add files to Writer Cloud** block and stores it in the state as the `file_info` state variable. This is the information about the uploaded file, specifically the `id` of the file in Writer Cloud, that will be used in the next blueprint.

    In the block's configuration panel, update the following fields:

    * **Link variable**: `file_info`
    * **Value type**: `text`
    * **Value**: `@{result}`. This references the `result` from the previous block, which is the information about the uploaded file.

      <img alt="Upload files to Writer Cloud" />
  </Step>
</Steps>

### Second blueprint: Parse and summarize the file

Once you've uploaded the file to the Writer Cloud, you can parse the PDF and provide the file's content to a text generation block.

<Steps>
  <Step title="Add a new blueprint">
    First, add a new blueprint to the canvas. Navigate to the Blueprint Layers section in the sidebar and click **+ Add blueprint** at the bottom of the blueprint layers outline.

    <img alt="Add blueprint" />
  </Step>

  <Step title="Add a UI trigger for the button to start summarizing">
    Navigate back to the Add Block section of the Agent Builder and add a **UI trigger** block to the new blueprint. This triggers the blueprint when a user clicks the button to start summarizing.

    In the block's configuration panel, update the following fields:

    * **Alias**: `Start summary` (optional, to make the trigger easier to identify)
    * **Component Id**: Select the `Summarize` button component from the dropdown of UI blocks
    * **Event type**: `wf-click`

      <img alt="Start summary trigger" />
  </Step>

  <Step title="Update the status message">
    Add a **Set state** block to the canvas. This block updates the status message in the state to indicate that the summarization is in progress. The message will appear in the UI message block.

    In the block's configuration panel, update the following fields:

    * **Link variable**: `status`
    * **Value type**: `text`
    * **Value**: `%Summarizing...`. The `%` symbol creates an animated spinning icon when the message appears in the UI.

      <img alt="Update status message" />
  </Step>

  <Step title="Add a block to parse the uploaded PDF file">
    Add a **Parse PDF Tool** block to the canvas. This block parses the uploaded PDF file so that the agent can generate a summary of the file.

    In the block's configuration panel, update the following fields:

    * **File**: `@{file_info.0.id}`. This references the `file_info` state variable you defined in the first blueprint and specifically access the `id` of the uploaded file.

    <Tip>
      The `file_info` state variable is an array of objects, containing one object with information from the **Add files to Writer Cloud** block. You can see the values of state variables by clicking the **State Explorer** (`<>`) icon at the top of the canvas. Learn more about [nested state variables](/agent-builder/state#nested-state-variables).
    </Tip>

    <img alt="Parse file" />
  </Step>

  <Step title="Add a block to generate a summary of the parsed file">
    Add a **Text generation** block to the canvas. This block generates text with a Palmyra LLM based on a prompt.

    In the block's configuration panel, update the following fields:

    * **Prompt**: `Provide a one-paragraph summary and a list of the three most important takeaways from this research paper: @{result}. Provide the summary for the user persona @{persona}.`
      * `@{result}` references the `result` from the previous block, which is the parsed PDF file.
      * `@{persona}` references the `persona` state variable you defined in the UI.
    * **Model**: `Palmyra X5`

    You can also update the additional fields **Temperature** and **Max tokens** to further control the output of the text generation.

    <img alt="Generate summary" />
  </Step>

  <Step title="Add the summary results to the state">
    Add a **Set state** block to the canvas. This block takes the results from the previous text generation block and stores it in the state as the `summary` state variable.

    In the block's configuration panel, update the following fields:

    * **Link variable**: `summary`
    * **Value type**: `text`
    * **Value**: `@{result}`. This references the `result` from the previous block, which is the summary of the parsed PDF file.

      <img alt="Set state summary" />
  </Step>

  <Step title="Clear the status message from the state">
    Finally, add a **Set state** block to the canvas to clear the status message from the state.

    In the block's configuration panel, update the following fields:

    * **Link variable**: `status`
    * **Value type**: `text`
    * **Value**: Leave blank. This clears the status message from the state.

      <img alt="Clear status message" />
  </Step>
</Steps>

## Preview the agent

Now that you've built the agent, you can preview it.

1. Navigate to the **Preview** tab in the Agent Builder.
2. Add a PDF file to the file input block and select a persona from the dropdown.
3. Click the **Summarize** button to start the summarization process.
4. You should see the status message update to `%Summarizing...` and the summary results appear in the text block.

The results take a few seconds to generate. You can see the progress of the Agent Builder by going to the **Blueprints** tab. The blueprint blocks that have already run will be highlighted in green, and the one that's currently running will have an animated blue border.

<img alt="Blueprint in progress" />

If you encounter any issues, refer to the [Troubleshooting](/agent-builder/troubleshooting) guide for debugging information.

<feedback />


# Sync agents between local and cloud
Source: https://dev.writer.com/agent-builder/sync-agent



This guide shows you how to synchronize your agents between local development and Writer Cloud environments. This allows local development offline and cloud development and deployment when you wish to share your agent with your team.

<Tip>To avoid conflicts, work in one environment at a time, then explicitly sync your changes to the other environment when ready. This ensures your changes are properly tracked and preserved across environments.</Tip>

## Overview

There are two main synchronization workflows:

1. **Cloud to local**: Start with a cloud agent and bring it to local development
2. **Local to cloud**: Start locally and push to the cloud

Both workflows involve downloading an agent's configuration from one environment and importing it into another.

Before starting, ensure you have:

* A Writer API key. Learn how to [create and manage API keys](/api-reference/api-keys).
* The `writer` package installed. Learn how to [install the Writer Framework package](/agent-builder/local-development#install-the-writer-framework-package).
* If you plan to do both local and cloud development, review [considerations for dual-environment development](#considerations-for-dual-environment-development) to properly handle environment variables and secrets for both local and cloud development.

## Cloud to local workflow

If you've already created an agent in the AI Studio Agent Builder web editor and want to continue development locally:

### Step 1: Export from cloud

From your cloud agent in Agent Builder, click the three dots in the top right corner of your agent and select **Download agent .zip file** to download a `.zip` file containing your agent's configuration.

<img alt="" />

<Note>The downloaded zip file contains your agent's UI components and blueprints in a hidden folder called `.wf`. If the zip file appears to only contain generic starter files, check for the `.wf` folder which contains the internal configuration files for your agent.</Note>

### Step 2: Create local project

Create a new local project with Writer Framework and import the cloud agent by unzipping the export file you downloaded:

```bash theme={null}
# Create a new local project
writer create my-cloud-agent
cd my-cloud-agent
```

### Step 3: Import the cloud agent into your local project

Import the cloud agent by unzipping the export file you downloaded into your local project:

```bash theme={null}
# The export file contains all your agent's configuration from the web editor
unzip your-agent-export.zip
```

<Tip>If you don't see the import/export agent functionality in your local development environment, make sure you're running the latest version of Writer Framework. Run `pip install --upgrade writer` to get the latest version with all features.</Tip>

Because the export file contains all your agent's configuration, the terminal asks you to confirm file-by-file whether you'd like to overwrite your local project's files.

Type `A` to replace all files in your local project with the exported agent.

You can also choose the following options:

* `y` to replace an individual file
* `r` to rename an individual file
* `N` to cancel the import and not replace any files

```bash theme={null}
my-agent unzip ~/Downloads/export.zip
Archive:  /Users/Downloads/export.zip
  inflating: README.md
replace main.py? [y]es, [n]o, [A]ll, [N]one, [r]ename: A
```

### Step 4: Set up environment variables

Set up a `.env` file in your local project directory and add your Writer API key:

```bash theme={null}
# .env
export WRITER_API_KEY="[your_api_key]"
```

Run the following command to activate the environment variables:

```bash theme={null}
source .env
```

### Step 5: Start local development

Start the local development server:

```bash theme={null}
writer edit .
```

Your local environment now contains an exact copy of your cloud agent, and you can continue development locally.

## Local to cloud workflow

To start development locally and then push to the cloud development environment, follow these steps:

### Step 1: Create local project

Start with a local project:

```bash theme={null}
writer create my-local-agent
cd my-local-agent
writer edit .
```

### Step 2: Develop locally

Build your agent locally using the Writer Framework development environment. You have full access to:

* Python code editing
* UI component configuration
* Blueprint design
* Local testing and debugging

### Step 3: Prepare for cloud sync

Before syncing to the cloud, ensure your code handles both Vault and environment variables as described in the [handle environment variables and secrets](#handle-environment-variables-and-secrets) section.

<Warning>Remove or move your `.env` file before syncing to prevent it from showing in the cloud editor.</Warning>

### Step 4: Download the local agent's export file

When you're ready to sync your local agent to the cloud, download the local agent's export file from the web editor.

In the visual editor interface, click the three dots in the top right corner of your agent and select **Download agent .zip file** to download a `.zip` file containing your agent's configuration.

<img alt="" />

<Tip>If you don't see the import/export agent functionality in your local development environment, make sure you're running the latest version of Writer Framework. Run `pip install --upgrade writer` to get the latest version with all features.</Tip>

The export file includes all files in your local project, including a hidden `.wf` folder that contains your UI components and blueprints as internal JSON files. Don't include sensitive information in the exported file because it is visible in the cloud editor.

### Step 5: Import the local agent into your cloud project

Open the Agent Builder web editor in AI Studio and click the three dots in the top right corner of your agent. Then select **Import agent .zip file** to upload the local agent's export file.

A modal appears and asks you to confirm that you are overwriting your existing agent.

<img alt="" />

Check the box to acknowledge that you're replacing your existing agent and click **Import** to continue.

## Deploy your agent

After syncing your agent to the cloud, deploy it following the [standard cloud workflow](/agent-builder/quickstart#deploy-the-agent).

From the agent editor interface in Agent Builder, click **Configure deployment** in the top right corner of the Agent Builder interface. You can also access the deployment configuration by going to the [AI Studio homepage](https://app.writer.com/aistudio) and selecting the agent you created.

If the agent isn't deployed, you see a toggle bar that says **Draft**.

<img alt="" />

To deploy the agent, toggle the bar. Writer deploys the agent to the Writer cloud, which takes a moment to complete.

When you deploy the agent, the toggle bar shows **Deployed**. It also shows a list of the teams in your organization, which you can use to grant access to the agent. You must select at least one team before you can view the URL for the deployed agent.

## Considerations for dual-environment development

* **Local-only libraries**: If you install Python packages locally that aren't listed in [Python libraries installed in Agent Builder](/agent-builder/python-libraries), your agent won't run in the cloud version of Agent Builder because those packages aren't available there.

* **Environment file visibility**: Your `.env` file is visible in plain text in the cloud editor when you sync your local project. Any file you add to your local project is included in the export file to the cloud. Before syncing, move your `.env` file to a different location or delete it to prevent it from showing in the cloud editor, and add your secrets to Vault.

* **Vault availability**: Vault is only available in the cloud version of Agent Builder. For local development, you must use environment variables. Structure your code to handle both scenarios gracefully.

* **Keep your local Writer Framework package up to date**: When you sync your agent to the cloud, the cloud version of Agent Builder uses the latest version of the Writer Framework package. If you're using a different version of the Writer Framework package in your local project, you might encounter errors when you sync to the cloud. To avoid this, keep your local Writer Framework package up to date by running the following command:

```bash theme={null}
pip install --upgrade writer
```

### Handle environment variables and secrets

The following code shows an example of how to look for secrets in Vault first, then fall back to environment variables:

```python theme={null}
import os

def get_secret(key, default=None):
    """Get a secret from Vault if available, otherwise from environment variables"""
    try:
        # Try to get from Vault first (cloud environment). The `vault` object is available in Python code blocks and in event handlers.
        return vault[key]
    except KeyError:
        # Fall back to environment variables (local development)
        return os.getenv(key, default)

# Usage example
api_key = get_secret("WRITER_API_KEY", "default_key")
database_url = get_secret("DATABASE_URL")
```

## Next steps

* Learn about [local development](/agent-builder/local-development) workflows
* Understand how to [deploy your agent](#deploy-your-agent) from the cloud
* Explore [custom Python code](/agent-builder/python-code) for advanced functionality

<feedback />


# Use tool calling in Agent Builder
Source: https://dev.writer.com/agent-builder/tool-calling

Add tool calling to Agent Builder blueprints and chatbots. Connect Knowledge Graphs and define custom functions with JSON Schema.

<Note>
  This page describes how to use tool calling in Agent Builder. If you're looking for an overview of tool calling, see [Introduction to tool calling](/agent-builder/tool-calling-intro).
</Note>

Agent Builder provides two ways to use tool calling:

* In a chatbot conversation with the [**Chat reply** block](/blueprints/chatreply). Use this block to add tool calling to your chatbot conversation.
* In a blueprint with the [**Tool calling** block](/blueprints/toolcalling). Use this block to add tool calling to your blueprint outside of a chatbot conversation.

<img alt="Tool calling block example showing a tool calling block connected to a Knowledge Graph and a function" />

Both blocks allow you to either connect to Knowledge Graphs or define the functions you want to provide to the model as [JSON Schema](https://json-schema.org/) objects. Then, when the model decides to use a tool, it either queries the Knowledge Graphs or sends the call to your blueprint, which executes the tool and sends the results back to the model.

## Add tool calling to your blueprint

To add tool calling to your blueprint, you need to:

1. [Add the **Tool calling** block to your blueprint](#add-the-tool-calling-block)
2. [Write a prompt that explains what the tool calling block is trying to accomplish](#write-a-prompt)
3. [Define the tools you want to provide to the model](#define-your-tools)
4. [Connect the tool calling block to the blocks that execute the tools](#connect-the-tool-calling-connectors-to-the-blocks-that-execute-the-tools)
5. [Add a **Return value** block to the end of the tool calling block](#add-a-return-value-block)

### Add the tool calling block

Add either the [**Tool calling** block](/blueprints/toolcalling) to your blueprint or the [**Chat reply** block](/blueprints/chatreply) to your chatbot conversation.

### Write a prompt

The prompt you write for the tool calling block in the **Prompt** field guides the model's behavior and decision making. It should explain what the tool calling block is trying to accomplish and what tools are available to use.

### Define your tools

Define the tools you want to provide to the **Tool calling** block, so that it knows what tools are available to use.

To add a new tool, click the **Add tool+** button in the **Tool calling** block's configuration menu.

In the **Add tool** modal that appears, select the **Tool type** from the dropdown, either:

* **Function**: A function that the model can call
* **Knowledge Graph**: A Knowledge Graph that the model can query

<Tabs>
  <Tab title="Function">
    If you're using a function, enter the function name and definition. See [tool definition](#tool-definition) for more details about how to define the function.

    <img alt="" />
  </Tab>

  <Tab title="Knowledge Graph">
    If you're using a Knowledge Graph, select the Knowledge Graph(s) you want to use from the dropdown of available Knowledge Graphs.

    <img alt="" />
  </Tab>
</Tabs>

<Note>
  If you are using the Knowledge Graph tool, you only need to select which Knowledge Graphs you want to use. You do not need to define the tool or connect it to the blocks that execute the tools as described below. The following steps are only for the Function tool type.
</Note>

#### Tool definition

The tool definition for a `Function` tool follows the [JSON Schema](https://json-schema.org/) format and has the following structure:

| Parameter               | Type   | Description                                                                                                                      |
| ----------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `name`                  | string | The name of the tool                                                                                                             |
| `description`           | string | A description of what the tool does and when the model should use it                                                             |
| `parameters`            | object | An object containing the tool's input parameters                                                                                 |
| `parameters.type`       | string | The type of the parameter, which is `object` for a JSON schema                                                                   |
| `parameters.properties` | object | An object containing the tool's parameters in the form of a [JSON schema](https://json-schema.org/). See below for more details. |
| `parameters.required`   | array  | An array of the tool's required parameters                                                                                       |

#### Example tool definition

Below is an example of a tool definition that gets the details of a package's shipping status. It takes a tracking number and a carrier name as input parameters and returns the shipping status of the package.

```json theme={null}
{
  "name": "check_shipping_status",
  "description": "Gets real-time shipping and tracking information for a package",
  "parameters": {
    "type": "object",
    "properties": {
      "tracking_number": {
        "type": "string",
        "description": "The shipping carrier's tracking number"
      },
      "carrier": {
        "type": "string",
        "enum": ["fedex", "ups", "usps", "dhl"],
        "description": "Shipping carrier name"
      }
    },
    "required": ["tracking_number", "carrier"]
  }
}
```

### Connect the tool calling connectors to the blocks that execute the tools

When you define a `Function` tool in the block's configuration, it adds a connection point, shown as a purple circle, to the block. This allows you to connect it to another block or chain of blocks that perform the tool's work.

<img alt="" />

The example above shows the following setup for a tool that, among other functions, can check the status of a package:

<Steps>
  <Step>
    Create an **HTTP Request** block that calls a shipping status API. Access the arguments to the tool call by the names you provided in the tool definition; for example, `@{tracking_number}` and `@{carrier}`.
  </Step>

  <Step>
    Add a **Return value** block to the end of the **HTTP Request** block to send the result back to the model.
  </Step>

  <Step>
    Provide a [tool definition](#example-tool-definition) to the **Tool calling** block that indicates there's a tool that can check the status of a package and it needs the tracking number and carrier name as input parameters.
  </Step>

  <Step>
    Connect the **HTTP Request** block to the **Tool calling** block at the `check_shipping_status` connection point.
  </Step>
</Steps>

When the model decides to use the tool, it sends the tool call to the **HTTP Request** block, which executes the tool. The **HTTP Request** block runs and passes the result to the **Return value** block, which sends the results back to the model

<Tip>
  Every `Function` tool call needs a **Return value** block at the end that sends the results back to the model.
</Tip>

When the **Tool calling** block completes all tool calls and integrates the results into the response, it moves to the next block in the blueprint that's connected to the `Success` transition. The `@{result}` variable is available in the next block with the tool calling block's final output.

## View tool calls logs

When a Tool Calling block executes, it provides a list of thoughts and actions that it took to complete the task. You can observe these in the logs of the blueprint.

Expand the **Logs** section at the bottom of the blueprint. Find the **Tool calling** log and click the **Trace** button to see the full trace of the tool calls.

<img alt="" />

The trace shows the series of thoughts, actions, and tool calls that the model made to complete the task.

<img alt="" />

<feedback />


# Understand tool calling
Source: https://dev.writer.com/agent-builder/tool-calling-intro

Learn how tool calling enables dynamic agent workflows. Understand ReAct patterns and why tool calling matters for intelligent agent orchestration.

Tool calling allows you to call external tools that you define and integrate them into your agent's responses.

Instead of requiring you to build static, hardcoded workflows, tool calling enables you to build agents that dynamically choose and orchestrate the right combination of tools based on the specific context of each request.

This document describes why tool calling matters and how it works behind the scenes.

Jump to [tool calling in Agent Builder](/agent-builder/tool-calling) to learn how to use tool calling in Agent Builder.

## Why tool calling matters

* **Traditional approach limitations**: With pre-defined API workflows, you'd need to hardcode every possible scenario: "If customer mentions shipping, call shipping API. If they mention returns, call returns API." This creates rigid, brittle systems that can't handle the complexity of real customer inquiries.

* **Tool calling's dynamic intelligence**:
  The model acts as an intelligent orchestrator, analyzing the customer's specific problem and dynamically selecting the most relevant tools. It handles parameter extraction, API sequencing, and data synthesis automatically.

* **Enables advanced reasoning patterns**: Tool calling naturally supports [ReAct (Reasoning and Acting)](https://klu.ai/glossary/react-agent-model) workflows, where the model can think through problems step-by-step, take actions by calling tools, observe the results, and then reason about what to do next.

### Customer service example

A customer writes to your support team:

> "I ordered two items last week but only received one and the item I got isn't working."

Your Agent Builder agent takes a customer's request and uses tool calling to dynamically select the most relevant tools to use. Available tools might include:

* `get_order_details(order_id)`
* `check_shipping_status(tracking_number)`
* `initiate_return_request(order_id, reason)`
* `check_inventory_status(product_id)`
* `create_support_ticket(customer_id, issue_type)`
* Support, policies, and other knowledge from your connected Knowledge Graphs

Given the tools provided to the model, it can review the user's request and automatically:

1. Extract the customer ID from context
2. Call `get_order_details()` to understand what was ordered
3. Call `check_shipping_status()` for the missing item
4. Look up return policies from your connected support Knowledge Graph
5. Synthesize all this information into a coherent, helpful response

## How tool calling works behind the scenes

While the AI model intelligently decides which tools to use and what parameters to call them with, the model doesn't actually execute the tools itself. Instead, there's a two-step handoff process:

* **The AI decides what to do**: The model analyzes the customer's request and says "I need to call `get_order_details` with order ID #12345"
* **Your system does the actual work**: Your blueprint receives this instruction, makes the real API call to your order database, and sends the results back to the AI

The AI then uses the results of the tool calls to continue reasoning and provide the final response.

### Project manager analogy

Think of it like a project manager coordinating subcontractors: The AI project manager understands the entire project scope and knows exactly which specialists to engage and what work needs to be done. When a customer has an issue, the AI says "I need the shipping contractor to check tracking #12345, then the inventory contractor to verify stock levels." But the AI doesn't do the actual work; each subcontractor (your systems) has their own expertise and tools to execute their piece, then reports back to the project manager who coordinates everything into the final deliverable.

<img alt="Tool calling example with contractor analogy" />

This architecture provides several benefits:

* **Security and control**: You maintain full control over your data and system access
* **Flexibility**: You can easily add new tools or modify existing ones without changing the AI
* **Reliability**: Each system component can be optimized and maintained independently
* **Compliance**: Sensitive operations remain within your controlled infrastructure

## Next steps

Now that you understand why tool calling matters and how it works behind the scenes, [learn how to use tool calling in Agent Builder](/agent-builder/tool-calling).

<feedback />


# Tool calling with external APIs
Source: https://dev.writer.com/agent-builder/tool-calling-tutorial



This tutorial walks through building an agent that can intelligently call external APIs to get information and make decisions based on the information. It takes the name of a city and calls several tools and APIs to determine if it's a good day to view the sunset in that location.

If you are unfamiliar with tool calling, you might want to read the [Tool calling introduction](/agent-builder/tool-calling-intro) first to get a sense of how it works.

<CardGroup>
  <Card title="Interface">
    <img alt="A preview of the agent interface showing a text input for city name and a submit button" />
  </Card>

  <Card title="Blueprint">
    <img alt="The complete blueprint showing the workflow with API calls and logic blocks" />
  </Card>
</CardGroup>

While this tutorial uses free public APIs to demonstrate tool calling fundamentals, the tool calling patterns are applicable to real-world enterprise scenarios. You can use these same techniques to connect agents with your CRM, databases, internal APIs, and other business systems.

## Agent overview

This agent connects with two free, publicly available APIs:

* [Open-meteo](https://open-meteo.com/en/docs) for weather information
* [Sunrise sunset](https://sunrise-sunset.org/api) for sunrise and sunset times

The agent uses tool calling to get the weather information and sunrise and sunset times in a particular city. It then uses the information to determine if it's a good day to view the sunset, and if so, suggests places to view it.

## Clear the demo agent

Before you start building, clear the demo agent that comes with every new Agent Builder project.

See instructions for [clearing the demo agent in the Agent Builder Quickstart](/agent-builder/quickstart#clear-the-demo-agent).

## Build the UI

The agent has a plain UI with a text input for the city, a button to submit the request, a message block to display a loading message, and a text block to display the results.

<img alt="" />

<Steps>
  <Step title="Add a text input">
    Drag a **Text Input** block to the canvas. In the block's configuration menu, update the following:

    * **Label**: `City`
    * **Link variable** under **Binding**: `city`

    This updates the label in the UI to `City` and binds whatever the user types in the text input to the `city` state variable.
  </Step>

  <Step title="Add a button to submit the request">
    Drag a **Button** block to the canvas. In the block's configuration menu, update the following:

    * **Label**: `Submit`
  </Step>

  <Step title="Add a message block to display a loading message">
    Drag a **Message** block to the canvas. In the block's configuration menu, update the following:

    * **Message**: `@{status}`

    This displays the `status` state variable in the UI if it's set. If there's no status, the block isn't visible.
  </Step>

  <Step title="Add a text area to display the result">
    Drag a **Text** block to the canvas. In the block's configuration menu, update the following:

    * **Text**: `@{final_result}`

    This displays the `final_result` state variable in the UI if it's set. If there's no `final_result`, the block isn't visible.
  </Step>
</Steps>

## Build the blueprint

The logic of the blueprint is as follows:

1. The UI Trigger block triggers the agent when the user clicks the button.
2. The set state block adds a loading status to the UI.
3. The tool calling block calls the available tools to get the current date and time, the weather information, and sunrise and sunset times. From the provided information, it makes a determination about whether it's a good day to view the sunset.
4. Once the tool calling block completes, the agent updates the state with the results and clears the loading message.

<Note>
  Tool calling is a powerful feature that combines the model's knowledge with real-time data and external APIs. The model can make intelligent decisions by analyzing the data it receives and drawing on its own knowledge to interpret results. For example, you don't need to manually parse timestamps or format weather data when you get a response from an API. You can provide the raw API responses and the model extracts and uses the relevant information appropriately.
</Note>

<img alt="" />

<Steps>
  <Step title="Add a UI Trigger block">
    Drag a **UI Trigger** block to the canvas. In the block's configuration menu, update the following:

    * **Component Id**: Select the **Submit** button from the dropdown of available components.
    * **Trigger**: `wf-click`

    This triggers the blueprint when the user clicks the Submit button.
  </Step>

  <Step title="Add a set state block to add a loading status to the UI">
    Drag a **Set State** block to the canvas. In the block's configuration menu, update the following:

    * **Link variable**: `status`
    * **Value**: `%Loading...`

    This sets the `status` state variable to `Loading...` with an animated spinner when the blueprint starts.
  </Step>

  <Step title="Add a tool calling block and set the prompt">
    Drag a **Tool Calling** block to the canvas. In the block's configuration menu, update the following:

    * **Prompt**:

    ```
    You are a travel assistant helping someone in @{city} find optimal sunset viewing opportunities.

    ## Available Tools
    - `get_weather` - Get weather conditions for today
    - `get_time` - Get current time as a UTC timestamp
    - `get_sunset_time` - Get sunrise/sunset times in UTC

    ## Decision Logic
    1. **Check timing**: Use `get_time` and `get_sunset_time` to compare current time with today's sunset
       - **CRITICAL**: Both times are returned in UTC. You MUST convert both to @{city}'s local timezone before comparing
       - **Time comparison steps**:
         a. Get current UTC time using `get_time`
         b. Get sunset UTC time using `get_sunset_time`
         c. Convert BOTH times to @{city}'s local timezone
         d. Compare the local times using 24-hour format
       - **If local current time > local sunset time**: Sunset has passed
       - **If local current time < local sunset time**: Sunset is upcoming
       - **Example**: If current UTC is 23:30 and sunset UTC is 22:45, convert both to local time first. If local current time is 19:30 and local sunset is 18:45, then sunset has passed.

    2. **Assess weather conditions**: Use `get_weather` to evaluate:
       - Temperature (comfort for outdoor viewing)
       - Cloud coverage (visibility of sun)
       - Wind speed (viewing comfort)
       - Precipitation chance (weather suitability)

    3. **Make recommendation**: If conditions are favorable, suggest 3 outdoor viewing locations in @{city}

    ## Tool Usage Notes
    - Both `get_weather` and `get_sunset_time` require latitude/longitude coordinates for @{city}
    - Use your knowledge to provide accurate coordinates
    - Remember to account for @{city}'s timezone when interpreting UTC timestamps

    ## Required Output Format
    Provide a structured response including:
    - **If sunset has passed**: Begin by stating "Sunset has already passed for today in @{city}" with the prompt to ask again tomorrow closer to sunset. Otherwise, ignore this section.
    - Current date/time in @{city}'s local timezone, in a user-friendly format
    - Sunset time in @{city}'s local timezone
    - Weather factor analysis with "Good" or "Bad" rating and the values for each factor:
      - Temperature
      - Cloud coverage  
      - Wind speed
      - Precipitation chance
    - Overall viewing recommendation
    - If favorable: 3 recommended locations with brief explanations

    ## Important Reminder
    Always convert UTC timestamps to the local timezone of @{city} before making any time comparisons. Never compare UTC times directly without conversion.
    ```

    This prompt instructs the agent to use the available tools to get the weather information and sunrise and sunset times. It then makes a determination about whether it's a good day to view the sunset. In the next steps, you'll define the tools and add the logic to call them.

    <Tip>
      While AI models are highly intelligent and can reason about complex tasks, a well-structured prompt is essential for effective tool calling. The prompt serves as a blueprint that clearly defines what tools are available, guides the model through the decision-making workflow, and specifies the expected output format. Without this guidance, the model might miss important steps, use tools inefficiently, or produce results that don't match your requirements. A detailed prompt ensures consistent, reliable performance across different inputs and scenarios. See [Prompting strategies](/home/prompting) for more suggestions about writing prompts.
    </Tip>
  </Step>

  <Step title="Define a tool to get the current date and time">
    Within the **Tool calling** block, click **Add tool+** to define a new tool for the agent to use. Define the tool as follows:

    * **Tool type**: `function`
    * **Tool name**: `get_time`
    * **Function tool definition**:

    ```json theme={null}
    {
      "description": "Gets the current time as a UTC timestamp",
      "parameters": {},
      "type": "function"
    }
    ```

    This defines a tool that the agent can use to get the current date and time in UTC. It doesn't require any parameters, so the parameters field is empty. The description field provides a brief description of what the tool does.
  </Step>

  <Step title="Add a Python block to provide the current date and time in UTC">
    This example uses a Python block to get the current date and time in UTC using Python's `datetime` library.

    Drag a **Python** block to the canvas. In the block's configuration menu, update the following:

    * **Code**:

    ```python theme={null}
    from datetime import datetime, timezone

    utc_datetime = datetime.now(timezone.utc)
    set_output(utc_datetime)
    ```

    <Warning>
      To return a value from a Python block, you must use the `set_output` function.
    </Warning>

    This returns the current date and time in UTC.
  </Step>

  <Step title="Add a return value block to return the date and time to the tool calling block">
    If you want a tool to return a value to the Tool Calling block, you need to add a **Return Value** block at the end of the tool's logic.

    Drag a **Return Value** block to the canvas. In the block's configuration menu, update the following:

    * **Value**: `@{result}`

    This returns the result of the Python block to the Tool Calling block.
  </Step>

  <Step title="Define a tool to get the weather information">
    Within the **Tool calling** block, click **Add tool+** to define a new tool for the agent to use. Define the tool as follows:

    * **Tool type**: `function`
    * **Tool name**: `get_weather`
    * **Function tool definition**:

    ```json theme={null}
    {
      "description": "Gets information about the weather for day. This includes cloud coverage, precipitation change, wind gusts, and temperature. The return value is a series of arrays for each of those variables, with one value for each hour. The arrays all start at GMT+0 for the first time.",
      "parameters": {
        "lat": {
          "type": "string",
          "description": "The latitude to request weather data for"
        },
        "long": {
          "type": "string",
          "description": "The longitude to request weather data for"
        }
      },
      "type": "function"
    }
    ```

    This defines a tool that the agent can use to get the weather information for a given latitude and longitude. It instructs the agent to provide the latitude and longitude in the parameters, because the API requires latitude and longitude.
  </Step>

  <Step title="Add an HTTP Request block to call the Open-meteo API">
    Now you need to add a block to call the Open-meteo API.

    Drag an **HTTP Request** block to the canvas. In the block's configuration menu, update the following:

    * **URL**: `https://api.open-meteo.com/v1/forecast?latitude=@{lat}&longitude=@{long}&hourly=temperature_2m,cloud_cover,precipitation_probability,precipitation,wind_speed_10m&forecast_days=1`

    This is the URL for the Open-meteo API. It includes the `@{lat}` and `@{long}` variables, which you defined as required parameters in the tool definition. The agent provides the latitude and longitude when it calls the tool.

    For more information about the Open-meteo API and the request this is making, see the [Open-meteo documentation](https://open-meteo.com/en/docs).
  </Step>

  <Step title="Add a return value block to return the weather information to the tool calling block">
    The HTTP Request block returns the weather information in JSON format. You need to add a **Return Value** block to return the weather information to the Tool Calling block.

    Drag a **Return Value** block to the canvas. In the block's configuration menu, update the following:

    * **Value**: `@{result}`

    This returns the result of the HTTP Request block to the Tool Calling block. The agent will be able to parse and use the JSON data that the HTTP Request block returns.
  </Step>

  <Step title="Define a tool to get the sunrise and sunset times">
    Within the **Tool calling** block, click **Add tool+** to define a new tool for the agent to use. Define the tool as follows:

    * **Tool type**: `function`
    * **Tool name**: `get_sunset_time`
    * **Function tool definition**:

    ```json theme={null}
    {
      "description": "Gets info for about the sunset and sunrise times at a given location based on latitude and longitude",
      "parameters": {
        "lat": {
          "type": "string",
          "description": "The latitude to request weather data for"
        },
        "long": {
          "type": "string",
          "description": "The longitude to request weather data for"
        }
      },
      "type": "function"
    }
    ```

    This defines a tool that the agent can use to get the sunrise and sunset times for a given latitude and longitude. It instructs the agent to provide the latitude and longitude in the parameters, because the API requires latitude and longitude.
  </Step>

  <Step title="Add an HTTP Request block to call the Sunrise Sunset API">
    Now you need to add a block to call the Sunrise Sunset API.

    Drag an **HTTP Request** block to the canvas. In the block's configuration menu, update the following:

    * **URL**: `https://api.sunrise-sunset.org/json?lat=@{lat}&lng=@{long}&tzid=UTC&date=today`

    This is the URL for the Sunrise Sunset API. It includes the `@{lat}` and `@{long}` variables, which you defined as required parameters in the tool definition. The agent provides the latitude and longitude when it calls the tool.

    For more information about the Sunrise Sunset API and the request this is making, see the [Sunrise Sunset documentation](https://sunrise-sunset.org/api).
  </Step>

  <Step title="Add a return value block to return the sunrise and sunset times to the tool calling block">
    The HTTP Request block returns the sunrise and sunset times in JSON format. You need to add a **Return Value** block to return the sunrise and sunset times to the Tool Calling block.

    Drag a **Return Value** block to the canvas. In the block's configuration menu, update the following:

    * **Value**: `@{result}`

    This returns the result of the HTTP Request block to the Tool Calling block. The agent will be able to parse and use the JSON data that the HTTP Request block returns.
  </Step>

  <Step title="Add a set state block to display the results">
    Now that you've defined the tools for the Tool Calling block and added the logic to call them, the agent can take the prompt and provided tools and make a determination about whether it's a good day to view the sunset.

    Once the Tool Calling block completes, the agent should display the final results in the UI. You can display the final results in the UI by setting the `final_result` state variable, which will be displayed in the UI's **Text** block.

    Drag a **Set State** block to the canvas. In the block's configuration menu, update the following:

    * **Link variable**: `final_result`
    * **Value**: `%@{result}`

    This sets the `final_result` state variable to the result of the Tool Calling block.
  </Step>

  <Step title="Add a set state block to clear the loading message">
    Finally, you need to add a block to clear the loading message when the Tool Calling block completes.

    Drag a **Set State** block to the canvas. In the block's configuration menu, update the following:

    * **Link variable**: `status`
    * **Value**: Leave empty to clear the state variable

    This clears the `status` state variable when the Tool Calling block completes.
  </Step>
</Steps>

## Preview the agent

Navigate to the **Preview** tab to test the agent.

Enter a city and click the Submit button to see the agent's response. You should first see the loading message, then the final results when the Tool Calling block completes.

<img alt="" />

You can see the agent's progress and reasoning in the **Logs** tab.

<img alt="" />

## Next steps

This tutorial demonstrates the basics of tool calling with external APIs. To continue improving on this agent, you might consider adding a **Structured Output** block after the Tool Calling block to ensure the agent returns the results in the correct format. You could also add more components to the UI to display the results in a more user-friendly way, such as a [**Metric**](/components/metric) block to display the weather conditions or different **Text** blocks to display the different sections of the results.

Next, you might want to learn more about tool calling:

* [Learn more about tool calling](/agent-builder/tool-calling)
* [Learn more about the Tool Calling block](/blueprints/toolcalling)
* [Learn more about the HTTP Request block](/blueprints/httprequest)

<feedback />


# Troubleshoot common issues
Source: https://dev.writer.com/agent-builder/troubleshooting



This page provides troubleshooting suggestions for debugging and observing your agents as you build them.

* [Track an agent's progress through a blueprint](#view-progress-through-a-blueprint)
* [View error and info logs](#view-error-and-info-logs)
* [Add additional log messages](#add-additional-logs)
* [View traces from a block's execution](#view-traces)
* [Inspect an agent's state](#inspect-agent-state)
* [Observe agent usage and performance](#observe-agent-usage-and-performance)

## View progress through a blueprint

Some blocks such as PDF parsing and text generation can take a while to run. As an agent runs, you can view its progress through the blueprint by navigating to the **Blueprint** tab.

The color of a blueprint block's border describes the status of its execution.

* **Green**: The block completed successfully
* **Animated blue**: The block is currently running
* **Red**: The block failed
* **No border**: The block hasn't run yet

The following image shows a blueprint with a PDF parsing block that's currently running. The previous blocks have completed successfully and the following blocks haven't run yet.

<img alt="Blueprint tab" />

You can also see the progress of the agent in the **Log** status bar. Learn more below.

## View error and info logs

If there are any errors or messages as your agent runs, you'll see an indication in the **Log** bar in the bottom right corner of the page.

You can click the **Log** bar to expand it and see more details.

<img alt="Log bar" />

### Add additional logs with Log Message blocks

You can add additional custom [**Log message** blocks](/blueprints/logmessage) to the agent for debugging purposes. Log messages are helpful to understand the flow of the agent and the value of state and other variables at a given point in the execution.

To add a log message, add a **Log message** block to the canvas. In the block's configuration panel, update the following fields:

* **Type**: `info` or `error`
* **Message**: The message to log

Below is an example of a log message block during a file parsing process. It logs the file ID before beginning the parsing process, to help you debug if the file isn't found or isn't parsed correctly.

<img alt="Log message block" />

When the block runs, you'll see the log message in the **Logs** bar at the bottom of the page.

<img alt="Log message in logs" />

### Add additional logs with Python code

You can use python `print` statements and the globally available [`logger` object](https://docs.python.org/3/library/logging.html) to add additional logs to the agent.

```python theme={null}
print("This is a log message that shows in logs as 'Captured stdout'")
logger.info("This is an info message that shows in logs under 'Captured logs'")
logger.warning("This is a warning message that shows in logs under 'Captured logs'")
logger.error("This is an error message that shows in logs under 'Captured logs'")
```

The following image shows an example of a Python block that logs messages via print statements and the `logger` object.

<img alt="Python block with logs" />

When the block runs, the log messages appear in the **Logs** bar at the bottom of the page.

<img alt="Log messages in logs" />

## View traces

The log bar shows information about the agent's execution as it runs and after blocks complete. It includes the following:

* The block name
* The status of the block: success, error, or in progress
* A link to view a trace at that point in the execution
* How long the block took to run

<img alt="Log bar with traces" />

The trace link opens a new tab with a detailed view of the agent's execution at that point in time. It contains:

* The value resulting from the block's execution, which is then added to the execution environment of the following block
* The return value, if the block has one
* The full execution environment at that point in time
* The call stack

Below is an example of a trace after a **File upload** block has completed.

<img alt="Trace of a file upload block" />

## Inspect agent state

You can use the **State explorer** to view an agent's state variables and their values. This is helpful when you're debugging an agent or need to check the state at a given point in the execution.

To access the state explorer, click the **State explorer** icon in the top right corner of the page.

<img alt="State explorer" />

## Observe agent usage and performance

You can view usage and performance metrics for your agent in the **Observability** tab. To get to this view, select the agent from the [AI Studio homepage](https://app.writer.com/ai-studio) and navigate to the **Observability** tab.

Here, you can view:

* Performance: Requests, errors, latency, and throughput
* Usage: Total requests, tokens, and cost, along with a geographic breakdown of requests
* Logs: Logs from individual requests

<feedback />


# Get your organization subscription details
Source: https://dev.writer.com/api-reference-legacy/billing/get-your-organization-subscription-details

get /billing/subscription

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Detect If the content is AI generated
Source: https://dev.writer.com/api-reference-legacy/content/detect-if-the-content-is-ai-generated

post /content/organization/{organizationId}/detect

<Warning>
  This endpoint is deprecated. Use the [AI detection](/api-reference/tool-api/ai-detect) endpoint instead.
</Warning>


# Post contentorganization team check
Source: https://dev.writer.com/api-reference-legacy/content/post-contentorganization-team-check

post /content/organization/{organizationId}/team/{teamId}/check

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Post contentorganization team correct
Source: https://dev.writer.com/api-reference-legacy/content/post-contentorganization-team-correct

post /content/organization/{organizationId}/team/{teamId}/correct

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Get cowriteorganization team template
Source: https://dev.writer.com/api-reference-legacy/cowrite/get-cowriteorganization-team-template

get /cowrite/organization/{organizationId}/team/{teamId}/template/{templateId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Post cowriteorganization team generate
Source: https://dev.writer.com/api-reference-legacy/cowrite/post-cowriteorganization-team-generate

post /cowrite/organization/{organizationId}/team/{teamId}/generate

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Delete snippets
Source: https://dev.writer.com/api-reference-legacy/snippet/delete-snippets

delete /snippet/organization/{organizationId}/team/{teamId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Find snippets
Source: https://dev.writer.com/api-reference-legacy/snippet/find-snippets

get /snippet/organization/{organizationId}/team/{teamId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Update snippets
Source: https://dev.writer.com/api-reference-legacy/snippet/update-snippets

put /snippet/organization/{organizationId}/team/{teamId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# List your styleguide pages
Source: https://dev.writer.com/api-reference-legacy/styleguide/list-your-styleguide-pages

get /styleguide/page

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Page details
Source: https://dev.writer.com/api-reference-legacy/styleguide/page-details

get /styleguide/page/{pageId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Add terms
Source: https://dev.writer.com/api-reference-legacy/terminology/add-terms

post /terminology/organization/{organizationId}/team/{teamId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Delete terms
Source: https://dev.writer.com/api-reference-legacy/terminology/delete-terms

delete /terminology/organization/{organizationId}/team/{teamId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Find terms
Source: https://dev.writer.com/api-reference-legacy/terminology/find-terms

get /terminology/organization/{organizationId}/team/{teamId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Update terms
Source: https://dev.writer.com/api-reference-legacy/terminology/update-terms

put /terminology/organization/{organizationId}/team/{teamId}

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# List users
Source: https://dev.writer.com/api-reference-legacy/user/list-users

get /user

<Warning>
  We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them.
</Warning>


# Manage API keys
Source: https://dev.writer.com/api-reference/api-keys

Learn how to create, manage, and delete API keys.

This guide helps you create and manage API keys for the Writer API. To create and manage API keys, you must be logged in to [AI Studio](https://app.writer.com/aistudio).

## About API keys

The Writer API uses token authentication for API requests. API keys are used as tokens, which you pass in the `Authorization` header of your requests:

```
Authorization: Bearer <api-key>
```

### API agents and API keys

Writer API keys are attached to AI Studio API agents.

API capabilities are set at the agent level. Within an API-based agent, you can create multiple API keys that share the capabilities of the agent. To create keys with different capabilities, create multiple API agents.

Each API key can be scoped to **All teams** or a **Specific team** within your organization. Team scoping controls which team can use a given key, while the agent's capabilities determine which API endpoints the key can access.

## Create an API agent

1. From the [AI Studio home page](https://app.writer.com/aistudio), open **Admin settings** in the navigation menu, then select **API Keys**. <img alt="" />
2. Click **Create API agent** in the top right corner of the page.
3. In the **Create an API agent** dialog, under **Who can use this API key by default**, choose **All teams** or **Specific team**:
   * For **All teams**, select a **Key owner**:

     <img alt="" />

   * For **Specific team**, choose the **Team with immediate access** and select a **Key owner**:

     <img alt="" />
4. Click **Create API agent**.
5. When the agent opens, select the title field to rename the agent, and enter a **short description** so you can recognize what the agent is for.

## Create an API key

Each API agent has a default API key, called `Production`. To create additional API keys:

1. Navigate to the API application. From the [AI Studio home page](https://app.writer.com/aistudio), click **Admin Settings** in the navigation menu, then select **API Keys**.
2. Click the API agent's tile you want to generate a new key for.
3. Click **Generate a new key**.
4. Configure the new key:
   * **API key name**: a descriptive name to help you identify the key.
   * Under **Who can use this API key by default**, choose **All teams** or **Specific team**:
     * For **All teams**, select a **Key owner**:

       <img alt="" />

     * For **Specific team**, choose the **Team with immediate access** and select a **Key owner**:

       <img alt="" />
5. Click **Generate a new key** to create the key.
6. Immediately after generating the key, copy the key and save it securely. You can't view the key again after you navigate away from this page.

<Info>
  Store the API key in a secure location, such as a `.env` file (for example, `WRITER_API_KEY=<API_KEY>`). If you lose a key, revoke it and generate a new one—you can't recover the original secret.
</Info>

## See your API agents and manage API keys

1. From the [AI Studio home page](https://app.writer.com/aistudio), open **Admin settings** → **API Keys**.
2. In the table, open an agent's row to go to that agent. <img alt="" />

<Info>
  You can't view or reveal the full API key after it's created. Copy it the first time it's shown and store it securely. To replace a lost key, generate a new key from the API agent page.
</Info>

## Manage API agent capabilities

API capabilities are set at the agent level and apply to all keys within that agent. To manage API agent capabilities:

1. From the [AI Studio home page](https://app.writer.com/aistudio), click **Admin Settings** in the navigation menu, then select **API Keys**.
2. Click an individual agent's tile to navigate to the agent.
3. Under **Capabilities**, toggle to enable or turn off a specific capability for the agent. <img alt="" />

<Info>Capabilities map to Writer API endpoints. Click **Read more** in the API Documentation column to learn more about the specific endpoints.</Info>

## Delete an API agent

To delete an API agent:

1. From the [AI Studio home page](https://app.writer.com/aistudio), open **Admin settings** → **API Keys**.
2. On the row for the agent you want to remove, open the **Actions** menu, then select **Delete**. <img alt="" />
3. Confirm the deletion by clicking **Delete**.

## Delete an API key

To delete an API key:

1. From the [AI Studio home page](https://app.writer.com/aistudio), open **Admin settings** → **API Keys**.
2. Open the agent in the table, then stay on the **Manage keys** tab.
3. On the row for the key you want to remove, open the **Actions** menu, then select **Revoke**. <img alt="" />
4. Confirm by clicking **Revoke key**.

## Next steps

* [Make your first API call](/home/quickstart) using your API key
* Read the [chat completion endpoint reference](/api-reference/completion-api/chat-completion)
* Review [API error codes](/api-reference/error-codes) if requests fail authentication


# Application details
Source: https://dev.writer.com/api-reference/application-api/application-details

get /v1/applications/{application_id}
Retrieves detailed information for a specific no-code agent (formerly called no-code applications), including its configuration and current status.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Retrieve all jobs
Source: https://dev.writer.com/api-reference/application-api/application-jobs

get /v1/applications/{application_id}/jobs
Retrieve all jobs created via the async API, linked to the provided application ID (or alias).

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Generate from application
Source: https://dev.writer.com/api-reference/application-api/applications

post /v1/applications/{application_id}
Generate content from an existing no-code agent (formerly called no-code applications) with inputs.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Generate from application (async)
Source: https://dev.writer.com/api-reference/application-api/generate-application-job

post /v1/applications/{application_id}/jobs
Generate content asynchronously from an existing no-code agent (formerly called no-code applications) with inputs.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Retrieve graphs
Source: https://dev.writer.com/api-reference/application-api/get-application-graphs

get /v1/applications/{application_id}/graphs
Retrieve Knowledge Graphs associated with a no-code agent that has chat capabilities.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Retrieve a single job
Source: https://dev.writer.com/api-reference/application-api/get-single-async-job

get /v1/applications/jobs/{job_id}
Retrieves a single job created via the Async API.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# List applications
Source: https://dev.writer.com/api-reference/application-api/list-applications

get /v1/applications
Retrieves a paginated list of no-code agents (formerly called no-code applications) with optional filtering and sorting capabilities.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Retry job execution
Source: https://dev.writer.com/api-reference/application-api/post-retry-async-job

post /v1/applications/jobs/{job_id}/retry
Re-triggers the async execution of a single job previously created via the Async api and terminated in error.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Associate graphs
Source: https://dev.writer.com/api-reference/application-api/put-application-graphs

put /v1/applications/{application_id}/graphs
Updates the list of Knowledge Graphs associated with a no-code chat agent.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>


# Chat completion
Source: https://dev.writer.com/api-reference/completion-api/chat-completion

post /v1/chat
Generate a chat completion based on the provided messages. The response shown below is for non-streaming. To learn about streaming responses, see the [chat completion guide](https://dev.writer.com/home/chat-completion).



# List models
Source: https://dev.writer.com/api-reference/completion-api/list-models

get /v1/models
Retrieve a list of available models that can be used for text generation, chat completions, and other AI tasks.



# Text generation
Source: https://dev.writer.com/api-reference/completion-api/text-generation

post /v1/completions
Generate text completions using the specified model and prompt. This endpoint is useful for text generation tasks that don't require conversational context.



# Understand error codes
Source: https://dev.writer.com/api-reference/error-codes



This page provides a comprehensive reference for all error codes, HTTP status codes, and error handling patterns used across Writer APIs.

## Error code reference

<Tip>
  **Quickstart**: Most issues fall into these categories. Check here first!
</Tip>

<CardGroup>
  <Card title="Authentication Issues" icon="shield-check">
    **401 Unauthorized**: check your API key format and permissions
  </Card>

  <Card title="Bad Request Data" icon="triangle-exclamation">
    **400 + validation\_error**: verify required fields and data types
  </Card>

  <Card title="Rate Limiting" icon="clock">
    **429 Too Many Requests**: implement exponential backoff
  </Card>

  <Card title="Server Problems" icon="server">
    **5xx Errors**: check our status page or retry your request
  </Card>
</CardGroup>

## Error types

Jump to specific error types to find the cause and suggested action:

* [Authentication and authorization errors](#authentication-and-authorization-errors)
* [Request validation errors](#request-validation-errors)
* [Rate limiting and quota errors](#rate-limiting-and-quota-errors)
* [Server and processing errors](#server-and-processing-errors)
* [Error codes by service](#error-codes-by-service)

## Error response format

All API endpoints return errors in a consistent JSON format:

```json theme={null}
{
  "tpe": "error_type",
  "errors": [
    {
      "description": "Human-readable error description",
      "key": "error.message.key",
      "extras": {}
    }
  ],
  "extras": {}
}
```

### Response structure

<Accordion title="🔧 Core fields (always present)">
  | Field    | Type   | Description                                                       |
  | -------- | ------ | ----------------------------------------------------------------- |
  | `tpe`    | string | Error type identifier (for example, "BadRequest", "Unauthorized") |
  | `errors` | array  | Array of error messages with details                              |
  | `extras` | object | Additional error context and metadata                             |
</Accordion>

<Accordion title="📋 Error message details">
  | Field         | Type   | Description                                     |
  | ------------- | ------ | ----------------------------------------------- |
  | `description` | string | Human-readable error description for developers |
  | `key`         | string | Message key for internationalization            |
  | `extras`      | object | Additional context specific to the error        |
</Accordion>

## HTTP status codes

Writer APIs use standard HTTP status codes to indicate the nature of errors:

| Status | Error code            | Type           | Description                                          | Common causes                           |
| ------ | --------------------- | -------------- | ---------------------------------------------------- | --------------------------------------- |
| 400    | `BadRequest`          | Validation     | Invalid request parameters or malformed request body | Missing required fields, invalid format |
| 401    | `Unauthorized`        | Authentication | Missing or invalid authentication credentials        | Missing API key, invalid format         |
| 403    | `Forbidden`           | Authorization  | Valid credentials but insufficient permissions       | Insufficient access rights              |
| 404    | `NotFound`            | Validation     | Requested resource does not exist                    | Invalid resource ID, deleted resource   |
| 409    | `Conflict`            | Validation     | Request conflicts with current state                 | Duplicate resource, version conflict    |
| 410    | `Gone`                | Validation     | Resource was permanently removed                     | Deleted/expired resource                |
| 413    | `PayloadTooLarge`     | Validation     | Request body too large                               | Exceeds configured size limits          |
| 422    | `UnprocessableEntity` | Validation     | Semantically invalid request                         | Fails business rules/constraints        |
| 429    | `TooManyRequests`     | Rate Limiting  | Rate limit or quota exceeded                         | Too many requests per time period       |
| 500    | `InternalServerError` | Server         | Internal server error                                | Server-side processing issue            |
| 503    | `ServiceUnavailable`  | Server         | Service temporarily unavailable                      | Maintenance, high load                  |
| 507    | `InsufficientStorage` | Server         | Insufficient storage to complete request             | Storage quota exhausted                 |

## Error codes by service

This section provides a comprehensive reference of all possible error codes organized by service and endpoint.

<AccordionGroup>
  <Accordion title="Chat Completions API" icon="comments">
    ### Text generation

    **Endpoint:** `POST /v1/completion`

    | Error Type                        | HTTP Status | Description              | Possible Causes                                          |
    | --------------------------------- | ----------- | ------------------------ | -------------------------------------------------------- |
    | `BadRequest`                      | 400         | Invalid model parameter  | Model name not supported or malformed                    |
    | `InvalidInputParameters`          | 400         | Invalid prompt format    | Empty prompt or invalid encoding                         |
    | `InvalidContentType`              | 400         | Invalid parameters       | Invalid `temperature`, `max_tokens`, or other parameters |
    | `BadCredentials`                  | 401         | Invalid API key          | Missing or malformed API key                             |
    | `BadToken`                        | 401         | API key expired          | API key has expired                                      |
    | `InsufficientAccessRights`        | 403         | Insufficient permissions | API key lacks required permissions                       |
    | `QuotaExceeded`                   | 429         | Rate limit exceeded      | Too many requests per minute                             |
    | `CompletionModelQuotaError`       | 429         | Quota exceeded           | Monthly usage limit reached                              |
    | `CompletionModelUnavailableError` | 500         | Model unavailable        | AI model temporarily unavailable                         |
    | `CompletionModelInternalError`    | 500         | Service overloaded       | High load, retry with backoff                            |

    ### Chat completions

    **Endpoint:** `POST /v1/chat/completions`

    | Error Type                          | HTTP Status | Description                   | Possible Causes                         |
    | ----------------------------------- | ----------- | ----------------------------- | --------------------------------------- |
    | `BadRequest`                        | 400         | Invalid messages format       | Malformed messages array or role        |
    | `InvalidInputParameters`            | 400         | Invalid model parameter       | Model not supported for chat            |
    | `InvalidContentType`                | 400         | Invalid tool configuration    | Malformed tools or `tool_choice`        |
    | `BadRequest`                        | 400         | Invalid streaming parameters  | Invalid stream or `stream_options`      |
    | `CompletionModelUnsafeRequestError` | 400         | Content policy violation      | Request contains prohibited content     |
    | `BadCredentials`                    | 401         | Authentication failed         | Invalid or missing API key              |
    | `BadToken`                          | 401         | Authentication failed         | Invalid or missing API key              |
    | `InsufficientAccessRights`          | 403         | Model access denied           | Account lacks access to requested model |
    | `QuotaExceeded`                     | 429         | Rate limit exceeded           | Too many requests per minute            |
    | `CompletionModelQuotaError`         | 429         | Model quota exceeded          | Model-specific usage limit reached      |
    | `CompletionModelInternalError`      | 500         | Model processing error        | Internal error in model inference       |
    | `CompletionModelUnavailableError`   | 503         | Model temporarily unavailable | Model service is down for maintenance   |
  </Accordion>

  <Accordion title="Files API" icon="file">
    ### Upload files

    **Endpoint:** `POST /v1/files`

    | Error Type                 | HTTP Status | Description                | Possible Causes                       |
    | -------------------------- | ----------- | -------------------------- | ------------------------------------- |
    | `BadRequest`               | 400         | Invalid file format        | Unsupported file type                 |
    | `InvalidInputParameters`   | 400         | File too large             | File exceeds size limit               |
    | `InvalidContentType`       | 400         | Invalid file content       | Corrupted or malformed file           |
    | `BadCredentials`           | 401         | Authentication required    | Missing or invalid API key            |
    | `BadToken`                 | 401         | Authentication required    | Missing or invalid API key            |
    | `InsufficientAccessRights` | 403         | Upload permission denied   | Account lacks file upload permissions |
    | `BadRequest`               | 413         | Payload too large          | Request body exceeds size limit       |
    | `QuotaExceeded`            | 429         | Upload rate limit exceeded | Too many file uploads per minute      |
    | `InternalError`            | 500         | File processing error      | Error processing uploaded file        |
    | `QuotaExceeded`            | 507         | Storage quota exceeded     | Account storage limit reached         |

    ### Get file

    **Endpoint:** `GET /v1/files/{file_id}`

    | Error Type                 | HTTP Status | Description             | Possible Causes                   |
    | -------------------------- | ----------- | ----------------------- | --------------------------------- |
    | `BadCredentials`           | 401         | Authentication required | Missing or invalid API key        |
    | `BadToken`                 | 401         | Authentication required | Missing or invalid API key        |
    | `InsufficientAccessRights` | 403         | Access denied           | No permission to access this file |
    | `ResourceNotFound`         | 404         | File not found          | File ID doesn't exist             |
    | `ResourceNotFound`         | 410         | File gone               | File has been deleted or expired  |
  </Accordion>

  <Accordion title="Knowledge Graphs API" icon="brain">
    ### Create graph

    **Endpoint:** `POST /v1/knowledge-graphs`

    | Error Type                 | HTTP Status | Description                 | Possible Causes                          |
    | -------------------------- | ----------- | --------------------------- | ---------------------------------------- |
    | `BadRequest`               | 400         | Invalid graph configuration | Malformed graph parameters               |
    | `InvalidInputParameters`   | 400         | Invalid graph name          | Name contains invalid characters         |
    | `BadRequest`               | 400         | Duplicate graph name        | Graph with this name already exists      |
    | `BadCredentials`           | 401         | Authentication required     | Missing or invalid API key               |
    | `BadToken`                 | 401         | Authentication required     | Missing or invalid API key               |
    | `InsufficientAccessRights` | 403         | Graph creation denied       | Account lacks graph creation permissions |
    | `QuotaExceeded`            | 429         | Graph creation rate limit   | Too many graphs created per hour         |
    | `InternalError`            | 500         | Graph creation failed       | Internal error creating graph            |

    ### Add file to graph

    **Endpoint:** `POST /v1/knowledge-graphs/{graph_id}/files`

    | Error Type                 | HTTP Status | Description                | Possible Causes                       |
    | -------------------------- | ----------- | -------------------------- | ------------------------------------- |
    | `BadRequest`               | 400         | Invalid file format        | File type not supported for graphs    |
    | `InvalidInputParameters`   | 400         | File processing failed     | Error parsing file content            |
    | `BadCredentials`           | 401         | Authentication required    | Missing or invalid API key            |
    | `BadToken`                 | 401         | Authentication required    | Missing or invalid API key            |
    | `InsufficientAccessRights` | 403         | Graph access denied        | No permission to modify this graph    |
    | `ResourceNotFound`         | 404         | Graph not found            | Graph ID does not exist               |
    | `BadRequest`               | 409         | File already in graph      | File is already associated with graph |
    | `QuotaExceeded`            | 429         | File processing rate limit | Too many files processed per minute   |

    ### Query graph

    **Endpoint:** `POST /v1/knowledge-graphs/{graph_id}/query`

    | Error Type                 | HTTP Status | Description               | Possible Causes                   |
    | -------------------------- | ----------- | ------------------------- | --------------------------------- |
    | `BadRequest`               | 400         | Invalid query format      | Malformed query parameters        |
    | `InvalidInputParameters`   | 400         | Query too complex         | Query exceeds complexity limits   |
    | `BadCredentials`           | 401         | Authentication required   | Missing or invalid API key        |
    | `BadToken`                 | 401         | Authentication required   | Missing or invalid API key        |
    | `InsufficientAccessRights` | 403         | Query permission denied   | No permission to query this graph |
    | `ResourceNotFound`         | 404         | Graph not found           | Graph ID doesn't exist            |
    | `QuotaExceeded`            | 429         | Query rate limit exceeded | Too many queries per minute       |
    | `InternalError`            | 500         | Query processing error    | Internal error processing query   |
  </Accordion>

  <Accordion title="Applications API" icon="gear">
    ### Create application

    **Endpoint:** `POST /v1/applications`

    | Error Type                 | HTTP Status | Description                       | Possible Causes                                |
    | -------------------------- | ----------- | --------------------------------- | ---------------------------------------------- |
    | `BadRequest`               | 400         | Invalid application configuration | Malformed application parameters               |
    | `InvalidInputParameters`   | 400         | Invalid blueprint configuration   | Blueprint syntax or configuration error        |
    | `BadRequest`               | 400         | Duplicate application name        | Application with this name already exists      |
    | `BadCredentials`           | 401         | Authentication required           | Missing or invalid API key                     |
    | `BadToken`                 | 401         | Authentication required           | Missing or invalid API key                     |
    | `InsufficientAccessRights` | 403         | Application creation denied       | Account lacks application creation permissions |
    | `QuotaExceeded`            | 429         | Application creation rate limit   | Too many applications created per hour         |
    | `InternalError`            | 500         | Application creation failed       | Internal error creating application            |

    ### Generate application job

    **Endpoint:** `POST /v1/applications/{app_id}/jobs`

    | Error Type                 | HTTP Status | Description              | Possible Causes                            |
    | -------------------------- | ----------- | ------------------------ | ------------------------------------------ |
    | `BadRequest`               | 400         | Invalid job parameters   | Malformed job request                      |
    | `InvalidInputParameters`   | 400         | Missing required inputs  | Required input parameters not provided     |
    | `BadCredentials`           | 401         | Authentication required  | Missing or invalid API key                 |
    | `BadToken`                 | 401         | Authentication required  | Missing or invalid API key                 |
    | `InsufficientAccessRights` | 403         | Job execution denied     | No permission to execute jobs for this app |
    | `ResourceNotFound`         | 404         | Application not found    | Application ID doesn't exist               |
    | `BadRequest`               | 409         | Job already running      | Application has a job already in progress  |
    | `QuotaExceeded`            | 429         | Job execution rate limit | Too many jobs executed per minute          |
    | `InternalError`            | 500         | Job execution failed     | Internal error executing job               |
  </Accordion>

  <Accordion title="Tools API" icon="wrench">
    ### PDF parser

    **Endpoint:** `POST /v1/tools/pdf-parser`

    | Error Type                 | HTTP Status | Description             | Possible Causes                      |
    | -------------------------- | ----------- | ----------------------- | ------------------------------------ |
    | `BadRequest`               | 400         | Invalid PDF file        | Corrupted or unsupported PDF         |
    | `InvalidInputParameters`   | 400         | PDF too large           | PDF exceeds size limits              |
    | `BadRequest`               | 400         | PDF password protected  | PDF requires password to access      |
    | `BadCredentials`           | 401         | Authentication required | Missing or invalid API key           |
    | `BadToken`                 | 401         | Authentication required | Missing or invalid API key           |
    | `InsufficientAccessRights` | 403         | Parser access denied    | Account lacks PDF parser permissions |
    | `QuotaExceeded`            | 429         | Parser rate limit       | Too many parsing requests per minute |
    | `InternalError`            | 500         | PDF parsing error       | Internal error parsing PDF           |

    ### Web search

    **Endpoint:** `POST /v1/tools/web-search`

    | Error Type                 | HTTP Status | Description                | Possible Causes                      |
    | -------------------------- | ----------- | -------------------------- | ------------------------------------ |
    | `BadRequest`               | 400         | Invalid search query       | Empty or malformed search query      |
    | `InvalidInputParameters`   | 400         | Search query too long      | Query exceeds maximum length         |
    | `BadCredentials`           | 401         | Authentication required    | Missing or invalid API key           |
    | `BadToken`                 | 401         | Authentication required    | Missing or invalid API key           |
    | `InsufficientAccessRights` | 403         | Search access denied       | Account lacks web search permissions |
    | `QuotaExceeded`            | 429         | Search rate limit          | Too many search requests per minute  |
    | `InternalError`            | 500         | Search processing error    | Internal error in web search         |
    | `InternalError`            | 503         | Search service unavailable | External search service is down      |
  </Accordion>

  <Accordion title="Vision API" icon="eye">
    ### Analyze images

    **Endpoint:** `POST /v1/vision/analyze`

    | Error Type                 | HTTP Status | Description                | Possible Causes                           |
    | -------------------------- | ----------- | -------------------------- | ----------------------------------------- |
    | `BadRequest`               | 400         | Invalid image format       | Unsupported image type                    |
    | `InvalidInputParameters`   | 400         | Image too large            | Image exceeds size limits                 |
    | `InvalidContentType`       | 400         | Invalid image content      | Corrupted or malformed image              |
    | `BadCredentials`           | 401         | Authentication required    | Missing or invalid API key                |
    | `BadToken`                 | 401         | Authentication required    | Missing or invalid API key                |
    | `InsufficientAccessRights` | 403         | Vision access denied       | Account lacks vision analysis permissions |
    | `QuotaExceeded`            | 429         | Vision rate limit          | Too many analysis requests per minute     |
    | `InternalError`            | 500         | Image processing error     | Internal error processing image           |
    | `InternalError`            | 503         | Vision service unavailable | Vision service is down for maintenance    |
  </Accordion>

  <Accordion title="Guardrails" icon="shield-halved">
    Guardrail errors occur when content is blocked by configured safety controls. For more information, see [Configure guardrails](/home/guardrails).

    ### Content blocked by guardrail

    | Error Type                 | HTTP Status | Description                     | Possible Causes                                                                                                                      |
    | -------------------------- | ----------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
    | `GuardrailBlockedError`    | 400         | Content blocked by guardrail    | Input or output violated guardrail policy                                                                                            |
    | `BadRequest`               | 400         | Bedrock guardrail violation     | Content blocked by AWS Bedrock guardrail policy (PII, content filter, denied topic)                                                  |
    | `BadRequest`               | 400         | Azure Content Safety violation  | Content blocked by Azure text moderation (harmful content category exceeded threshold) or Prompt Shields (prompt injection detected) |
    | `BadCredentials`           | 401         | Guardrail authentication failed | Invalid AWS credentials for Bedrock guardrail or invalid Azure API key for Content Safety                                            |
    | `InsufficientAccessRights` | 403         | Guardrail access denied         | Missing permissions for guardrail provider                                                                                           |
    | `ResourceNotFound`         | 404         | Guardrail not found             | Invalid guardrail ID or version (Bedrock) or invalid endpoint URL (Azure Content Safety)                                             |
    | `InternalError`            | 500         | Guardrail processing error      | Error communicating with guardrail provider                                                                                          |
    | `InternalError`            | 503         | Guardrail service unavailable   | Guardrail provider service is down                                                                                                   |

    **Example guardrail error:**

    ```json theme={null}
    {
      "tpe": "BadRequest",
      "errors": [
        {
          "description": "Content blocked by guardrail: PII detected",
          "key": "fail.guardrail.blocked",
          "extras": {
            "guardrail_name": "pii-filter",
            "entity_type": "CREDIT_CARD"
          }
        }
      ],
      "extras": {}
    }
    ```

    **How to Fix:**

    * Review the blocked entity type or violation reason in the error response
    * Remove or mask sensitive content before retrying
    * Check guardrail configuration if blocks are unexpected
    * See [Guardrails documentation](/home/guardrails) for configuration options
  </Accordion>
</AccordionGroup>

## Error categories

### Authentication and authorization errors

#### 401 Unauthorized

| Status | Error code       | Type           | Description     | Common causes                                        |
| ------ | ---------------- | -------------- | --------------- | ---------------------------------------------------- |
| 401    | `BadCredentials` | Authentication | Invalid API key | Missing Authorization header, invalid API key format |
| 401    | `BadToken`       | Authentication | API key expired | API key has expired                                  |

**Example Response:**

```json theme={null}
{
  "tpe": "Unauthorized",
  "errors": [
    {
      "description": "Invalid token",
      "key": "fail.unauthorized.badToken",
      "extras": {}
    }
  ],
  "extras": {}
}
```

**How to Fix:**

* Verify your API key is correct
* Ensure you've included the `Authorization: Bearer <your_api_key>` header
* Check that your API key hasn't expired
* See [API Keys documentation](/api-reference/api-keys) for more details

#### 403 Forbidden

| Status | Error code                 | Type          | Description                        | Common causes                                |
| ------ | -------------------------- | ------------- | ---------------------------------- | -------------------------------------------- |
| 403    | `InsufficientAccessRights` | Authorization | API key lacks required permissions | Insufficient permissions, wrong API key type |

**Example Response:**

```json theme={null}
{
  "tpe": "Forbidden",
  "errors": [
    {
      "description": "Insufficient access rights",
      "key": "fail.forbidden.insufficientAccessRights",
      "extras": {}
    }
  ],
  "extras": {}
}
```

**How to Fix:**

* Verify your API key has the required permissions
* Check if you're using the correct API key type
* Contact support if permissions seem incorrect

### Request validation errors

#### 400 Bad Request

| Status | Error code               | Type       | Description                | Common causes                             |
| ------ | ------------------------ | ---------- | -------------------------- | ----------------------------------------- |
| 400    | `BadRequest`             | Validation | Invalid request parameters | Missing required fields, invalid format   |
| 400    | `InvalidInputParameters` | Validation | Invalid parameter values   | Out of range values, unsupported options  |
| 400    | `InvalidContentType`     | Validation | Invalid content type       | Malformed JSON, wrong content-type header |

**Example Response:**

```json theme={null}
{
  "tpe": "BadRequest",
  "errors": [
    {
      "description": "Invalid input parameters",
      "key": "fail.badRequest.invalidParameters",
      "extras": {}
    }
  ],
  "extras": {}
}
```

**How to Fix:**

* Check required fields and data formats
* Validate parameter values against API documentation
* Ensure request body is valid JSON
* See specific endpoint documentation for required parameters

#### 404 Not Found

| Status | Error code         | Type       | Description                      | Common causes                         |
| ------ | ------------------ | ---------- | -------------------------------- | ------------------------------------- |
| 404    | `ResourceNotFound` | Validation | Requested resource doesn't exist | Invalid resource ID, deleted resource |

**Example Response:**

```json theme={null}
{
  "tpe": "NotFound",
  "errors": [
    {
      "description": "Resource is not found",
      "key": "fail.notFound.resource",
      "extras": {}
    }
  ],
  "extras": {}
}
```

**How to Fix:**

* Verify the resource ID is correct
* Check if the resource has been deleted
* Ensure the endpoint URL is correct

### Rate limiting and quota errors

#### 429 Too Many Requests

| Status | Error code                  | Type          | Description                      | Common causes                          |
| ------ | --------------------------- | ------------- | -------------------------------- | -------------------------------------- |
| 429    | `QuotaExceeded`             | Rate Limiting | Too many requests in time period | Exceeding per-minute or per-day limits |
| 429    | `CompletionModelQuotaError` | Rate Limiting | Model quota exceeded             | Monthly usage limit reached            |

**Example Response:**

```json theme={null}
{
  "tpe": "TooManyRequests",
  "errors": [
    {
      "description": "Quota exceeded",
      "key": "fail.tooManyRequests.quotaExceeded",
      "extras": {}
    }
  ],
  "extras": {}
}
```

**How to Fix:**

* Implement exponential backoff
* Check your rate limits in the dashboard
* Consider upgrading your plan for higher limits
* See [Rate Limits documentation](/api-reference/rate-limits) for details

### Server and processing errors

#### 500 Internal Server Error

| Status | Error code                     | Type   | Description             | Common causes                     |
| ------ | ------------------------------ | ------ | ----------------------- | --------------------------------- |
| 500    | `InternalError`                | Server | Unexpected server error | Server-side processing issue      |
| 500    | `CompletionModelInternalError` | Server | Model processing error  | Internal error in model inference |

**Example Response:**

```json theme={null}
{
  "tpe": "InternalServerError",
  "errors": [
    {
      "description": "Internal server error",
      "key": "fail.internalServerError.generic",
      "extras": {}
    }
  ],
  "extras": {}
}
```

**How to Fix:**

* Retry the request after a short delay
* Check if the issue persists
* Contact support if the error continues

#### 503 Service Unavailable

| Status | Error code                        | Type   | Description                   | Common causes                         |
| ------ | --------------------------------- | ------ | ----------------------------- | ------------------------------------- |
| 503    | `CompletionModelUnavailableError` | Server | Model temporarily unavailable | Model service is down for maintenance |

**Example Response:**

```json theme={null}
{
  "tpe": "ServiceUnavailable",
  "errors": [
    {
      "description": "Service temporarily unavailable",
      "key": "fail.serviceUnavailable.temporary",
      "extras": {}
    }
  ],
  "extras": {}
}
```

**How to Fix:**

* Wait and retry after some time
* Check service status page
* Implement retry logic with exponential backoff

## AI model-specific errors

For AI content generation endpoints, the API may return additional error types:

### Model errors

| Error Type                              | Description                          |
| --------------------------------------- | ------------------------------------ |
| `CompletionModelUnavailableError`       | AI model is temporarily unavailable  |
| `CompletionModelInternalError`          | Internal error in model processing   |
| `CompletionModelQuotaError`             | Model usage quota exceeded           |
| `CompletionModelUnauthorizedError`      | Invalid model access credentials     |
| `CompletionModelUnderlyingRequestError` | Error in underlying model request    |
| `CompletionModelUnsafeRequestError`     | Request contains unsafe content      |
| `CompletionModelNoPredictionError`      | Model couldn't generate a prediction |

**Example model error:**

```json theme={null}
{
  "tpe": "CompletionModelQuotaError",
  "errors": [
    {
      "description": "Model usage quota exceeded",
      "key": "fail.model.quotaExceeded",
      "extras": {}
    }
  ],
  "extras": {}
}
```

## SDK error types

The official Writer SDKs provide structured error handling with specific exception types:

### Python SDK errors

| Error Type                 | HTTP Status | Description                                          |
| -------------------------- | ----------- | ---------------------------------------------------- |
| `APIConnectionError`       | N/A         | Network connectivity issues, timeouts, DNS failures  |
| `APITimeoutError`          | N/A         | Request timeout exceeded                             |
| `BadRequestError`          | 400         | Invalid request parameters or malformed request body |
| `AuthenticationError`      | 401         | Missing or invalid API key                           |
| `PermissionDeniedError`    | 403         | Valid API key but insufficient permissions           |
| `NotFoundError`            | 404         | Requested resource doesn't exist                     |
| `UnprocessableEntityError` | 422         | Request is well-formed but contains semantic errors  |
| `RateLimitError`           | 429         | Rate limit or quota exceeded                         |
| `InternalServerError`      | ≥500        | Internal server error                                |

### JavaScript/Node.js SDK errors

| Error Type              | HTTP Status | Description                                          |
| ----------------------- | ----------- | ---------------------------------------------------- |
| `connection_error`      | N/A         | Network connectivity issues, timeouts, DNS failures  |
| `timeout_error`         | N/A         | Request timeout exceeded                             |
| `bad_request`           | 400         | Invalid request parameters or malformed request body |
| `authentication_error`  | 401         | Missing or invalid API key                           |
| `permission_denied`     | 403         | Valid API key but insufficient permissions           |
| `not_found`             | 404         | Requested resource doesn't exist                     |
| `unprocessable_entity`  | 422         | Request is well-formed but contains semantic errors  |
| `rate_limit_error`      | 429         | Rate limit or quota exceeded                         |
| `internal_server_error` | ≥500        | Internal server error                                |

### Error handling example

This example shows how to handle common error cases when making API requests using the Writer SDKs.

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer
  import writerai

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  try:
      response = client.chat.chat(
          messages=[
              {
                  "role": "user",
                  "content": "Write a haiku about programming"
              }
          ],
          model="palmyra-x5"
      )
      print("Success:", response.choices[0].message.content)
      
  except writerai.APIConnectionError as e:
      print("Network error:", e)
      print("Cause:", e.__cause__)
      
  except writerai.RateLimitError as e:
      print("Rate limited:", e)
      print("Status code:", e.status_code)
      # SDK automatically handles retries with exponential backoff
      
  except writerai.AuthenticationError as e:
      print("Authentication failed:", e)
      print("Status code:", e.status_code)
      # Refresh API key or redirect to login
      
  except writerai.BadRequestError as e:
      print("Invalid request:", e)
      print("Status code:", e.status_code)
      
  except writerai.InternalServerError as e:
      print("Server error:", e)
      print("Status code:", e.status_code)
      # SDK automatically retries 5xx errors
      
  except writerai.APIStatusError as e:
      print("API error:", e)
      print("Status code:", e.status_code)
      print("Response:", e.response)
  ```

  ```javascript JavaScript theme={null}
  import Writer from '@writer/writer-node';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer({});

  try {
    const response = await client.chat.chat({
      messages: [
        {
          role: 'user',
          content: 'Write a haiku about programming'
        }
      ],
      model: 'palmyra-x5'
    });
    
    console.log('Success:', response.choices[0].message.content);
  } catch (error) {
    if (error.code === 'bad_request') {
      console.error('Invalid request:', error.message);
    } else if (error.code === 'authentication_error') {
      console.error('Authentication failed:', error.message);
      // Refresh API key or redirect to login
    } else if (error.code === 'rate_limit_error') {
      console.error('Rate limited:', error.message);
      // SDK automatically handles retries with exponential backoff
    } else if (error.code === 'internal_server_error') {
      console.error('Server error:', error.message);
      // SDK automatically retries 5xx errors
    } else if (error.code === 'connection_error') {
      console.error('Network error:', error.message);
    } else {
      console.error('Unexpected error:', error.message);
    }
  }
  ```
</CodeGroup>

## Troubleshooting common errors

### 400 Bad request

* Verify all required parameters are included
* Check parameter value formats and constraints
* Ensure request body is valid JSON

### 401 Unauthorized

* Verify API key is correct and not expired
* Check that the API key has the required permissions
* Ensure the Authorization header is properly formatted

### 403 Forbidden

* Check if your organization/team has access to the requested resource
* Verify that the feature is enabled for your plan
* Contact support if you believe this is an error

### 429 Too many requests

* Implement exponential backoff retry logic
* Check your current usage against rate limits
* Consider upgrading your plan for higher limits

### 500 Internal server error

* Retry the request after a short delay
* Check the [Writer status page](https://status.writer.com/) for known issues
* Contact support if the error persists

## Next steps

* [API keys](/api-reference/api-keys): learn how to authenticate with Writer APIs
* [Rate limits](/api-reference/rate-limits): understand API rate limiting and quotas
* [Python SDK](https://github.com/writer/writer-python): official Python library with built-in error handling
* [Node.js SDK](https://github.com/writer/writer-node): official JavaScript/Node.js library with built-in error handling


# Handle errors
Source: https://dev.writer.com/api-reference/error-handling



<Info>
  This table outlines the HTTP status codes you may encounter when using the
  API, along with descriptions to help you understand and troubleshoot issues.
</Info>

| Error code | Description                                                                                                                                                               |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **400**    | **Bad request error:** Indicates an issue with the format or content of your request. This error may also encompass other 4XX status errors not specifically listed here. |
| **401**    | **Authentication error:** There's an issue with your API key, or it's missing from your request headers resulting in unauthorized access.                                 |
| **403**    | **Permission error:** Your API key doesn't have permission to access the requested resource.                                                                              |
| **404**    | **Model not found error:** The specified model couldn't be found. This may be due to an incorrect URL or model ID.                                                        |
| **429**    | **Rate limit exceeded:** You have exceeded the allotted number of requests per time period for your account. Please wait and try your request again later.                |
| **500**    | **API error:** An unexpected error occurred within our systems. If this persists, contact support for further assistance.                                                 |
| **503**    | **Service unavailable:** The server is currently unable to handle the request due to a temporary overload or maintenance. Please retry your request after some time.      |
| **504**    | **Timeout error:** The server didn't receive a timely response from an upstream server. Please retry your request after some time.                                        |

**Best practices**

<CardGroup>
  <Card title="Validate requests" icon="check">
    Ensure that all requests are correctly formatted and include all necessary
    parameters and authentication information before sending them to the server. You can check the [API reference](/api-reference) for the correct information.
  </Card>

  <Card title="Check API key" icon="key">
    Ensure that you are using a valid API key, specific to either the API/SDK or Writer Framework. Use the [API Quickstart](/home/quickstart) for the former and the [Writer Framework Quickstart](/framework/quickstart) for the latter.
  </Card>

  <Card title="Check for firewalls" icon="fire">
    Ensure that firewalls or security settings are not blocking the Writer API or your application or service using the Writer API or SDK.
  </Card>

  <Card title="Monitor and log errors" icon="monitor-waveform">
    Continuously monitor and log errors on your side to identify patterns or
    recurring issues that may require changes to your application or further
    investigation.
  </Card>
</CardGroup>


# Delete file
Source: https://dev.writer.com/api-reference/file-api/delete-file

delete /v1/files/{file_id}
Permanently delete a file from the system. This action cannot be undone.



# Download file
Source: https://dev.writer.com/api-reference/file-api/download-file

get /v1/files/{file_id}/download
Download the binary content of a file. The response will contain the file data in the appropriate MIME type.



# Retry failed files
Source: https://dev.writer.com/api-reference/file-api/files-retry

post /v1/files/retry
Retry processing of files that previously failed to process. This will re-attempt the processing of the specified files.



# List files
Source: https://dev.writer.com/api-reference/file-api/get-all-files

get /v1/files
Retrieve a paginated list of files with optional filtering by status, graph association, and file type.



# Retrieve file
Source: https://dev.writer.com/api-reference/file-api/get-file

get /v1/files/{file_id}
Retrieve detailed information about a specific file, including its metadata, status, and associated graphs.



# Upload file
Source: https://dev.writer.com/api-reference/file-api/upload-files

post /v1/files
Upload a new file to the system. Supports various file formats including PDF, DOC, DOCX, PPT, PPTX, JPG, PNG, EML, HTML, SRT, CSV, XLS, and XLSX.



# Add file to graph
Source: https://dev.writer.com/api-reference/kg-api/add-file-to-graph

post /v1/graphs/{graph_id}/file
Add a file to a Knowledge Graph.

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>


# Create graph
Source: https://dev.writer.com/api-reference/kg-api/create-graph

post /v1/graphs
Create a new Knowledge Graph.



# Delete graph
Source: https://dev.writer.com/api-reference/kg-api/delete-graph

delete /v1/graphs/{graph_id}
Delete a Knowledge Graph.

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>


# List graphs
Source: https://dev.writer.com/api-reference/kg-api/list-graphs

get /v1/graphs
Retrieve a list of Knowledge Graphs.

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>


# Question
Source: https://dev.writer.com/api-reference/kg-api/question

post /v1/graphs/question
Ask a question to specified Knowledge Graphs.

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>


# Remove file from graph
Source: https://dev.writer.com/api-reference/kg-api/remove-file-from-graph

delete /v1/graphs/{graph_id}/file/{file_id}
Remove a file from a Knowledge Graph.

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>


# Retrieve graph
Source: https://dev.writer.com/api-reference/kg-api/retrieve-graph

get /v1/graphs/{graph_id}
Retrieve a Knowledge Graph.

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>


# Update graph
Source: https://dev.writer.com/api-reference/kg-api/update-graph

put /v1/graphs/{graph_id}
Update the name and description of a Knowledge Graph.

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>


# Migrate from web search API to web search tool
Source: https://dev.writer.com/api-reference/migration-guides/web-search



This guide shows you how to migrate from the deprecated [web search API endpoint](/api-reference/tool-api/web-search) to the [web search tool](/home/web-search-tool) in chat completions. After completing these steps, you can search the web using the web search tool, which provides the same capabilities within a chat completion workflow.

## Compare the APIs

The web search API and the web search tool provide similar search results, but the tool integrates web search into conversational workflows and requires specifying queries and configuration differently. The table below compares the two APIs.

| Aspect                | Web search API                                                                            | Web search tool                                                                                                                                                                                                                                                              |
| --------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Endpoint**          | `/v1/tools/web-search`                                                                    | `/v1/chat` with the prebuilt [web search tool](/home/web-search-tool) specification                                                                                                                                                                                          |
| **Request structure** | Pass search parameters such as `query`, `search_depth`, etc. directly in the request body | Provide the search query as part of the conversation `messages` and add the [web search tool](/home/web-search-tool) in the `tools` array. Additional parameters (like domains to include or exclude) are specified in the tool configuration, not as direct request fields. |
| **Response format**   | Results in `answer` and `sources` fields                                                  | Answer in `choices[0].message.content` and data in `web_search_data` field                                                                                                                                                                                                   |
| **Parameter control** | Explicit API parameters                                                                   | Most parameters specified in message prompt; only `include_domains` and `exclude_domains` as tool configuration                                                                                                                                                              |

## Migrate your code

The tabs below show a request using the web search API and the same request using the web search tool.

<Tabs>
  <Tab title="Before: Web search API">
    The web search API accepts search parameters directly in the request body. The following example shows an example request using the web search API. View the same request using the web search tool in the [after migrating](#after-migrating) section.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --location 'https://api.writer.com/v1/tools/web-search' \
        --header 'Content-Type: application/json' \
        --header "Authorization: Bearer $WRITER_API_KEY" \
        --data '{
          "query": "What are the most significant developments in artificial intelligence this year?",
          "include_domains": ["wikipedia.org", "github.com"],
          "exclude_domains": ["quora.com"],
          "search_depth": "advanced",
          "max_results": 10
        }'
      ```

      ```python Python theme={null}
      from writerai import Writer

      # Initialize the Writer client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      response = client.tools.web_search(
        query="What are the most significant developments in artificial intelligence this year?",
        include_domains=["wikipedia.org", "github.com"],
        exclude_domains=["quora.com"],
        search_depth="advanced",
        max_results=10
      )

      print(response.answer)
      ```

      ```javascript JavaScript theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the Writer client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const response = await client.tools.webSearch({
        query: "What are the most significant developments in artificial intelligence this year?",
        include_domains: ["wikipedia.org", "github.com"],
        exclude_domains: ["quora.com"],
        search_depth: "advanced",
        max_results: 10
      });

      console.log(response.answer);
      ```
    </CodeGroup>

    **Response:**

    ```json theme={null}
    {
      "query": "What are the most significant developments in artificial intelligence this year?",
      "answer": "This year has seen several significant developments...",
      "sources": [
        {
          "url": "https://www.ibm.com/think/insights/artificial-intelligence-trends",
          "raw_content": null
        }
      ]
    }
    ```
  </Tab>

  <Tab title="After: Web search tool">
    The web search tool uses the chat completions endpoint with a web search tool specification. The model handles the search based on your message and tool configuration. Parameters like `search_depth` and `max_results` are now specified in the message prompt:

    <CodeGroup>
      ```bash cURL theme={null}
      curl --location 'https://api.writer.com/v1/chat' \
        --header 'Content-Type: application/json' \
        --header "Authorization: Bearer $WRITER_API_KEY" \
        --data '{
          "model": "palmyra-x5",
          "messages": [
            {
              "role": "user",
              "content": "What are the most significant developments in artificial intelligence this year? Use advanced search depth and limit to 10 results."
            }
          ],
          "tool_choice": "auto",
          "tools": [
            {
              "type": "web_search",
              "function": {
                "include_domains": ["wikipedia.org", "github.com"],
                "exclude_domains": ["quora.com"]
              }
            }
          ]
        }'
      ```

      ```python Python theme={null}
      from writerai import Writer

      # Initialize the Writer client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      messages = [{"role": "user", "content": "What are the most significant developments in artificial intelligence this year? Use advanced search depth and limit to 10 results."}]

      tools = [{
        "type": "web_search",
        "function": {
          "include_domains": ["wikipedia.org", "github.com"],
          "exclude_domains": ["quora.com"]
        }
      }]

      response = client.chat.chat(
        model="palmyra-x5", 
        messages=messages, 
        tools=tools,
        tool_choice="auto"
      )

      print(response.choices[0].message.content)
      ```

      ```javascript JavaScript theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the Writer client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const messages = [{role: "user", content: "What are the most significant developments in artificial intelligence this year? Use advanced search depth and limit to 10 results."}];

      const tools = [{
        type: "web_search",
        function: {
          include_domains: ["wikipedia.org", "github.com"],
          exclude_domains: ["quora.com"]
        }
      }];

      const response = await client.chat.chat({
        model: "palmyra-x5", 
        messages: messages, 
        tools: tools,
        tool_choice: "auto"
      });

      console.log(response.choices[0].message.content);
      ```
    </CodeGroup>

    **Response:**

    ```json theme={null}
    {
      "id": "2ac04514",
      "object": "chat.completion",
      "choices": [
        {
          "index": 0,
          "finish_reason": "tool_calls",
          "message": {
            "content": "The latest developments in AI technology are happening at a rapid pace...",
            "role": "assistant",
            "web_search_data": {
              "search_depth": "advanced",
              "sources": [
                {
                  "url": "https://en.wikipedia.org/wiki/AI_boom",
                  "raw_content": null
                }
              ]
            }
          }
        }
      ]
    }
    ```
  </Tab>
</Tabs>

## Map your parameters

The following table shows how web search API parameters map to the web search tool. To customize search behavior in the web search tool, include your requirements in the message prompt.

| Web search API        | Web search tool                                                                      |
| --------------------- | ------------------------------------------------------------------------------------ |
| `query`               | Include in the message `content` field                                               |
| `include_domains`     | `tools[0].function.include_domains`                                                  |
| `exclude_domains`     | `tools[0].function.exclude_domains`                                                  |
| `search_depth`        | Include in the message prompt (for example, "perform an advanced search")            |
| `max_results`         | Include in the message prompt (for example, "limit to 10 sources")                   |
| `time_range`          | Include in the message prompt (for example, "from the last week")                    |
| `include_raw_content` | Include in the message prompt (for example, "include raw source text")               |
| `topic`               | Include in the message prompt (for example, "search news articles")                  |
| `country`             | Include in the message prompt (for example, "localize results to the United States") |
| `chunks_per_source`   | Include in the message prompt (for example, "extract 3 text segments per source")    |

<Note>
  With the web search tool, most search parameters are specified in your message prompt rather than as separate API parameters. The model interprets your requirements and configures the search accordingly.

  Only `include_domains` and `exclude_domains` are available as tool configuration parameters.
</Note>

## Access search metadata

The web search tool response includes search metadata, including sources and raw text if requested, in the `web_search_data` field:

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  messages = [{"role": "user", "content": "What are the most significant developments in artificial intelligence this year? Use advanced search depth and limit to 10 results."}]

  tools = [{
    "type": "web_search",
    "function": {
      "include_domains": ["wikipedia.org", "github.com"],
      "exclude_domains": ["quora.com"]
    }
  }]

  response = client.chat.chat(
    model="palmyra-x5", 
    messages=messages, 
    tools=tools,
    tool_choice="auto"
  )

  # Get the search answer
  answer = response.choices[0].message.content

  # Get search metadata
  search_metadata = response.choices[0].message.web_search_data
  search_depth = search_metadata.search_depth
  sources = search_metadata.sources

  print(f"Answer: {answer}")
  print(f"Search depth: {search_depth}")
  print(f"Found {len(sources)} sources")

  for source in sources:
      print(f"- {source.url}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const messages = [{role: "user", content: "What are the most significant developments in artificial intelligence this year? Use advanced search depth and limit to 10 results."}];

  const tools = [{
    type: "web_search",
    function: {
      include_domains: ["wikipedia.org", "github.com"],
      exclude_domains: ["quora.com"]
    }
  }];

  const response = await client.chat.chat({
    model: "palmyra-x5", 
    messages: messages, 
    tools: tools,
    tool_choice: "auto"
  });

  // Get the search answer
  const answer = response.choices[0].message.content;

  // Get search metadata
  const searchMetadata = response.choices[0].message.web_search_data;
  const searchDepth = searchMetadata.search_depth;
  const sources = searchMetadata.sources;

  console.log(`Answer: ${answer}`);
  console.log(`Search depth: ${searchDepth}`);
  console.log(`Found ${sources.length} sources`);

  sources.forEach(source => {
    console.log(`- ${source.url}`);
  });
  ```
</CodeGroup>

## Explore related features

Learn more about the web search tool and related features:

* [Web search tool guide](/home/web-search-tool)
* [Chat completion guide](/home/chat-completion)
* [Other prebuilt tools](/home/tool-calling)
* [Streaming responses](/home/streaming)


# Paginate API results
Source: https://dev.writer.com/api-reference/pagination



The Writer API supports pagination for list endpoints. By default, the API returns 50 results per page. See below for more information about how to paginate your API requests.

## Pagination parameters

The following parameters are available for pagination in the API:

| Parameter | Type      | Description                                                                                                                                                |
| --------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`   | `integer` | The number of items to return per page. The default is 50. The maximum is 100.                                                                             |
| `order`   | `string`  | The order in which to return the items, based on the `created_at` field for the resource. The default is `desc`. The possible values are `asc` and `desc`. |
| `before`  | `string`  | The ID of first object in the previous page. This parameter instructs the API to return the previous page of results.                                      |
| `after`   | `string`  | The ID of the last object in the next page. This parameter instructs the API to return the next page of results.                                           |

## Pagination response

The API returns the following pagination information in the response body:

| Parameter  | Type      | Description                                        |
| ---------- | --------- | -------------------------------------------------- |
| `has_more` | `boolean` | Indicates whether there are more pages of results. |
| `first_id` | `string`  | The ID of the first object in the collection.      |
| `last_id`  | `string`  | The ID of the last object in the collection.       |

## Pagination example: Retrieving files

### Retrieve two most recently created files

The following cURL command retrieves the two most recently created files.

```bash theme={null}
curl -X GET "https://api.writer.com/v1/files?limit=2" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json"
```

The response body contains the two most recently created files, along with the pagination information:

* The `has_more` parameter is `true`, indicating that there are more files to retrieve
* The `first_id` parameter is the ID of the first file in the collection
* The `last_id` parameter is the ID of the last file in the collection

```json theme={null}
{
  "data": [
    {
      "id": "c6006e1f",
      "created_at": "2025-06-04T18:19:12.495984Z",
      "name": "paper.pdf",
      "graph_ids": [],
      "status": "completed"
    },
    {
      "id": "57c3754c",
      "created_at": "2025-06-04T18:19:09.973779Z",
      "name": "meeting_notes.pdf",
      "graph_ids": [],
      "status": "completed"
    }
  ],
  "has_more": true,
  "first_id": "c6006e1f",
  "last_id": "57c3754c"
}
```

### Retrieve the next page of results

To retrieve the next page of results, you can use the `after` parameter to specify the ID of the last object in the previous page.

```bash theme={null}
curl -X GET "https://api.writer.com/v1/files?limit=2&after=57c3754c" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json"
```

## SDK support

The Python and JavaScript SDKs provide iterators that automatically handle pagination for you. See more information about pagination in the [Python SDK](https://github.com/writer/writer-python/blob/main/README.md#pagination) and [JavaScript SDK](https://github.com/writer/writer-node/blob/main/README.md#pagination) documentation.


# Understand rate limits
Source: https://dev.writer.com/api-reference/rate-limits

Understand API rate limits for Writer endpoints. Learn about RPM and TPM limits, best practices, and retry strategies for optimal performance.

<Info>
  For custom rate limits, **[contact Sales](https://go.writer.com/contact-sales)**.
</Info>

To ensure optimal service performance and fairness in resource allocation, our endpoints enforce the following rate limits.

1. **RPM (requests per minute)**: 400
2. **TPM (token per min)**: 25,000

## Best practices

<AccordionGroup>
  <Accordion title="Monitor request rates" icon="monitor-waveform">
    Implement mechanisms in your applications to track and regulate the
    frequency of your requests to stay within the prescribed limits.
  </Accordion>

  <Accordion title="Adaptive retry strategies" icon="rotate-right">
    In cases where you exceed these limits, employ adaptive retry strategies
    with exponential backoff to handle retries efficiently and reduce the
    likelihood of consecutive limit breaches.
  </Accordion>

  <Accordion title="Response to HTTP 429 status codes" icon="code">
    Prepare to handle HTTP 429 (too many requests) responses by pausing or
    slowing down request rates.
  </Accordion>
</AccordionGroup>


# Secure your integration
Source: https://dev.writer.com/api-reference/security



Ensure compliance and secure communications between Writer and your server. This page covers transport-layer security (TLS) for API traffic. For data-at-rest encryption and key management, see [Encryption Key Management](/home/encryption-key-management).

Writer API supports TLS 1.3 by default and TLS 1.2+ in other cases.

## Using TLS and HTTPS

TLS refers to the process of securely transmitting data between the client—the app or browser that your customer is using—and your server. This was originally performed using the SSL (Secure Sockets Layer) protocol. However, this is outdated and no longer secure, and has been replaced by TLS. The term “SSL” continues to be used colloquially when referring to TLS and its function to protect transmitted data.

Payment pages must make use of a modern version of TLS (for example, TLS 1.2) as it significantly reduces the risk of you or your customers being exposed to a man-in-the-middle attack. TLS attempts to accomplish the following:

* Encrypt and verify the integrity of traffic between the client and your server
* Verify that the client is communicating with the correct server. In practice, this usually means verifying that the owner of the domain and the owner of the server are the same entity. This helps prevent man-in-the-middle attacks. Without it, there’s no guarantee that you’re encrypting traffic to the right recipient.

Additionally, your customers are more comfortable sharing sensitive information on pages visibly served over HTTPS, which can help increase your customer conversion rate.

If need be, you can test your integration without using HTTPS, and enable it once you’re ready to accept live charges. However, all interactions between your server and Writer must use TLS 1.2 (i.e, when using our libraries).

## Setting up TLS

SERVING RESOURCES SECURELY\
You should make sure that any resources (JavaScript, CSS, images, etc.) are also served over TLS to avoid a mixed content warning being shown to your customers in their browser.

A digital certificate—a file issued by a certification authority (CA)—is needed in order to use TLS. When installed, this certificate assures the client that it’s really communicating with the server it expects to be talking to, not an impostor. You should get a digital certificate from a reputable certificate provider, such as:

* Let’s Encrypt
* DigiCert
* NameCheap\
  Certificates can vary in cost, depending on the type of certificate and provider. Let’s Encrypt is a certificate authority that provides certificates for free.

Conceptually, setting up TLS is very straightforward: a certificate is purchased from a suitable provider, and then your server is configured to use it. The actual process does tend to be somewhat complex, and we recommend you follow the installation guide of the provider you use.

As TLS is a complex suite of cryptographic tools, it's easy to miss a few details. We recommend using the SSL Server Test by Qualys SSL Labs to make sure you have everything set up in a secure way.

## Additional security considerations

It can be a security risk to include JavaScript from other sites as your security becomes dependent on theirs. If they’re ever compromised, an attacker may be able to execute arbitrary code on your page. In practice, many sites make use of JavaScript for services like Google Analytics, even on secure pages. Nonetheless, it’s something to be aware of, and ideally minimize.

If you’re making use of webhooks, we recommend using TLS for the endpoint to avoid traffic being intercepted and the notifications altered (sensitive information is never included in a webhook event).

While complying with the Data Security Standards is important, it shouldn’t be where you stop thinking about security. Some good resources to learn about web security are:

* OWASP
* SANS
* NIST\
  Out-of-scope card data that you can safely store\
  Writer returns non-sensitive card information in the response to a charge request. This includes the card type, the last four digits of the card, and the expiration date. This information is not subject to PCI compliance, so you are able to store any of these properties in your database.


# Parse PDF
Source: https://dev.writer.com/api-reference/tool-api/pdf-parser

post /v1/tools/pdf-parser/{file_id}
Parse PDF to other formats.

<Warning>
  **Deprecation notice**: The [parse PDF API endpoint](/api-reference/tool-api/pdf-parser) at `/v1/tools/pdf-parser/{file_id}` is deprecated and will be removed on **December 22, 2025**.

  **Migration path**: We plan to introduce a prebuilt PDF parsing tool for chat completions that will provide similar functionality. This tool will work similarly to other prebuilt tools like the [LLM tool](/home/model-delegation). We will provide more details about this alternative when it becomes available.
</Warning>


# Web search
Source: https://dev.writer.com/api-reference/tool-api/web-search

post /v1/tools/web-search
Search the web for information about a given query and return relevant results with source URLs.

<Warning>
  **Deprecation notice**: The [web search API endpoint](/api-reference/tool-api/web-search) at `/v1/tools/web-search` is deprecated and will be removed on **December 22, 2025**.

  **Migration path**: Use the [web search tool](/home/web-search-tool) in chat completions to perform web searches during conversations. The web search tool provides the same search capabilities within a chat completion workflow. See the [migration guide](/api-reference/migration-guides/web-search) for detailed instructions.
</Warning>


# View supported languages
Source: https://dev.writer.com/api-reference/translation-api/language-support



Writer's translation capabilities support translating text from one language to another. Below are the supported languages and the [formality](#formality), [length control](#length-control), and [profanity masking](#profanity-masking) parameters.

## Supported languages

To specify a language for translation, use the [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language code. For example, `en` for English, `zh` for Chinese, `fr` for French, `es` for Spanish. If the language has a variant, the code appends the two-digit [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). For example, Mexican Spanish is `es-MX`.

The following languages are supported for translation:

| Language              | Language code |
| --------------------- | ------------- |
| Afrikaans             | `af`          |
| Albanian              | `sq`          |
| Amharic               | `am`          |
| Arabic                | `ar`          |
| Armenian              | `hy`          |
| Azerbaijani           | `az`          |
| Bengali               | `bn`          |
| Bosnian               | `bs`          |
| Bulgarian             | `bg`          |
| Catalan               | `ca`          |
| Chinese (Simplified)  | `zh`          |
| Chinese (Traditional) | `zh-TW`       |
| Croatian              | `hr`          |
| Czech                 | `cs`          |
| Danish                | `da`          |
| Dari                  | `fa-AF`       |
| Dutch                 | `nl`          |
| English               | `en`          |
| Estonian              | `et`          |
| Farsi (Persian)       | `fa`          |
| Filipino, Tagalog     | `tl`          |
| Finnish               | `fi`          |
| French                | `fr`          |
| French (Canada)       | `fr-CA`       |
| Georgian              | `ka`          |
| German                | `de`          |
| Greek                 | `el`          |
| Gujarati              | `gu`          |
| Haitian Creole        | `ht`          |
| Hausa                 | `ha`          |
| Hebrew                | `he`          |
| Hindi                 | `hi`          |
| Hungarian             | `hu`          |
| Icelandic             | `is`          |
| Indonesian            | `id`          |
| Irish                 | `ga`          |
| Italian               | `it`          |
| Japanese              | `ja`          |
| Kannada               | `kn`          |
| Kazakh                | `kk`          |
| Korean                | `ko`          |
| Latvian               | `lv`          |
| Lithuanian            | `lt`          |
| Macedonian            | `mk`          |
| Malay                 | `ms`          |
| Malayalam             | `ml`          |
| Maltese               | `mt`          |
| Marathi               | `mr`          |
| Mongolian             | `mn`          |
| Norwegian (Bokmål)    | `no`          |
| Pashto                | `ps`          |
| Polish                | `pl`          |
| Portuguese (Brazil)   | `pt`          |
| Portuguese (Portugal) | `pt-PT`       |
| Punjabi               | `pa`          |
| Romanian              | `ro`          |
| Russian               | `ru`          |
| Serbian               | `sr`          |
| Sinhala               | `si`          |
| Slovak                | `sk`          |
| Slovenian             | `sl`          |
| Somali                | `so`          |
| Spanish               | `es`          |
| Spanish (Mexico)      | `es-MX`       |
| Swahili               | `sw`          |
| Swedish               | `sv`          |
| Tamil                 | `ta`          |
| Telugu                | `te`          |
| Thai                  | `th`          |
| Turkish               | `tr`          |
| Ukrainian             | `uk`          |
| Urdu                  | `ur`          |
| Uzbek                 | `uz`          |
| Vietnamese            | `vi`          |
| Welsh                 | `cy`          |

## Formality

Formal translations use language associated with formal, polite communication, such as formal forms of second person pronouns and their verb agreement. Informal translations use language associated with informal, casual communication, such as informal forms of second person pronouns and their verb agreement.

The following languages support formal and informal translations. Set the `formality` parameter to `true` to use a formal translation, and `false` to use an informal translation.

| Language              | Language code |
| --------------------- | ------------- |
| Dutch                 | `nl`          |
| French                | `fr`          |
| French (Canada)       | `fr-CA`       |
| German                | `de`          |
| Hindi                 | `hi`          |
| Italian               | `it`          |
| Japanese              | `ja`          |
| Korean                | `ko`          |
| Portuguese (Portugal) | `pt-PT`       |
| Spanish               | `es`          |
| Spanish (Mexico)      | `es-MX`       |

## Length control

Sometimes, translated text is longer than the original source text. You can use length control to limit the length of the translated text. Set the `length_control` parameter to `true` to enable length control.

Length control is available for the following languages:

| Language            | Language code |
| ------------------- | ------------- |
| French              | `fr`          |
| German              | `de`          |
| Italian             | `it`          |
| Portuguese (Brazil) | `pt`          |
| Spanish             | `es`          |

## Profanity masking

You can choose to mask profanity in the translated text for all languages **except the languages listed in the table below**.

The following languages do not support profanity masking:

| Language   | Language code |
| ---------- | ------------- |
| Bengali    | `bn`          |
| Hindi      | `hi`          |
| Malayalam  | `ml`          |
| Punjabi    | `pa`          |
| Sinhala    | `si`          |
| Vietnamese | `vi`          |


# Add files to Writer Cloud
Source: https://dev.writer.com/blueprints/addfilestowritercloud



Uploads files to the Writer platform.

<img alt="" />

## Overview

The **Add files to Writer Cloud** block uploads files to the Writer cloud so you can use them in your workflows. It accepts a list of file objects as inputs.

## File inputs

The **Add files to Writer Cloud** block accepts a list of file objects as inputs. Each file object must have the following fields:

| Field  | Type   | Description                                                                                                    |
| ------ | ------ | -------------------------------------------------------------------------------------------------------------- |
| `name` | string | The name of the file.                                                                                          |
| `data` | bytes  | The content of the file as bytes.                                                                              |
| `type` | string | The [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types) of the file. |

### Adding files from the file input block

If you upload a file with the **File input** interface block, the list of file objects is automatically created for you, so all you have to do is pass the list to the **Add files to Writer Cloud** block. See the [example below](#example%3A-upload-files-to-writer-cloud-from-the-file-input-block) for how to do this.

You can retrieve the file objects list from the **File input** interface block in two ways:

* Connect a UI Trigger to the **File input** block's `wf-file-change` event. Then, use the `@{payload}` environment variable to access the file objects. The trigger will fire when a user adds or removes files from the file input block.
* Under **Binding** in the **File input** interface block, define a state variable that contains the list of file objects. Then reference that variable in the **Add files to Writer Cloud** block.

## Output

Once the files are uploaded to the Writer cloud, the **Add files to Writer Cloud** block returns a list containing the uploaded file IDs and other information about the files.

You can access the output of an **Add files to Writer Cloud** block using the `@{result}` variable in the block that follows it in a blueprint.

Each file object has the following fields:

| Field        | Type   | Description                                                             |
| ------------ | ------ | ----------------------------------------------------------------------- |
| `id`         | string | The ID of the file.                                                     |
| `name`       | string | The name of the file.                                                   |
| `status`     | string | The status of the file. Can be `in_progress`, `completed`, or `failed`. |
| `created_at` | string | The date and time the file was created.                                 |
| `graph_ids`  | array  | The IDs of any Knowledge Graphs that the file is associated with.       |

```json theme={null}
[
    {
        "id": "701118ae",
        "name": "research_paper.pdf",
        "status": "in_progress",
        "created_at": "2025-06-17T16:18:42.672407+00:00",
        "graph_ids": []
    }
]
```

### File processing status

If the uploaded file's status is `in_progress`, the file is being processed and is not ready to be used in a workflow.

Some files are processed quickly, such as PDFs and images, and are ready to be used in a workflow within seconds. Others, such as Word documents, may take a few minutes to process.

There are a few ways you can handle files that are still being processed:

* Split the blueprint into two parts: one that uploads the files when a user adds them to the file input block, and another that uses the files when a user clicks a button in the UI. This way, you can use the files in the workflow as soon as they're uploaded, and wait for the user to click a button to use the files in the second part of the blueprint. See an example in the [Upload, parse, and summarize PDFs](/agent-builder/summarize-pdfs#build-the-blueprints) tutorial.
* Add a Python block that introduces a few seconds of delay before the workflow continues.
* Add a [**Tool calling** block](/agent-builder/tool-calling) that can check the status of the file [using the Writer API](/api-reference/file-api/get-file) and wait for the file to be processed before continuing.

## Example: Upload files to Writer Cloud from the file input block

This example shows how to upload files to Writer Cloud that are stored in a state variable. In this example, the interface contains a file input block that's bound to the state variable `files`. After the UI Trigger fires, the **Add files to Writer Cloud** block uploads the files to Writer Cloud by accessing the `@{files}` state variable.

<img alt="" />

The `@{files}` state variable looks like this:

```json theme={null}
[
    {
        "name": "research_paper.pdf",
        "data": "...",
        "type": "application/pdf"
    }
]
```

## Example: Create and upload files from Python processing

This example shows how to manually create file objects in a **Python code** block and then upload them to Writer Cloud. In this scenario, a Python block generates a CSV report from data processing and creates the file object programmatically.

<img alt="" />

The **Python code** block contains the following code, which creates a CSV file object and returns it as a list for the **Add files to Writer Cloud** block to access.

```python theme={null}
import csv
import io

# Sample data processing
data = [
    ["Name", "Score", "Category"],
    ["Product A", 85, "Electronics"],
    ["Product B", 92, "Home & Garden"],
    ["Product C", 78, "Electronics"]
]

# Create CSV content
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerows(data)
csv_content = csv_buffer.getvalue()

# Convert to bytes
csv_bytes = csv_content.encode('utf-8')

# Create file object
file_object = {
    "name": "product_analysis.csv",
    "data": csv_bytes,
    "type": "text/csv"
}

# Return the file object as a list (required by Add files to Writer Cloud block)
set_output([file_object])
```

The Python block outputs `[file_object]`, which the **Add files to Writer Cloud** block then accesses using `@{result}` to upload the CSV file.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Files</td>
      <td>Object</td>
      <td>-</td>

      <td>
        <code>\[]</code>
      </td>

      <td>A list of files to be uploaded to the Writer platform. You can use files uploaded via the File input component or specify dictionaries with data, type and name.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>File successfully uploaded.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error uploading the files.</td>
    </tr>
  </tbody>
</table>

Access the output of a **Add files to Writer Cloud** block using the `@{result}` variable in the block that follows it in a blueprint.


# Add to Knowledge Graph
Source: https://dev.writer.com/blueprints/addtoknowledgegraph



Adds structured information to the knowledge graph. Use for storing facts AI can reference.

<img alt="" />

## Overview

The **Add to Knowledge Graph** block ingests files into a Writer Knowledge Graph, making the information available for AI-powered search, retrieval, and question answering. You can use this block to build enterprise AI applications that need to reference company documents, policies, or structured data.

<Tip>
  The Add to Knowledge Graph block only works for Knowledge Graphs that support file uploads. You can't add files to Knowledge Graphs that support data connectors, like Google Drive or Notion, or URLs.
</Tip>

**Supported file types:** You can upload `PDF`, `TXT`, `DOC/DOCX`, `PPT/PPTX`, `EML`, `HTML`, `SRT`, `CSV`, or `XLS/XLSX` files to a Knowledge Graph.

## How it works

1. **File selection**: Choose the files to ingest into the Knowledge Graph.
2. **Graph specification**: Select the target Knowledge Graph for ingestion.
3. **Processing**: The block processes and indexes the file content.
4. **Integration**: Files become available for AI-powered queries and retrieval.

The block handles the complete process of uploading files to Writer and adding them to the Knowledge Graph, including file validation, content extraction, and indexing. You don't need to upload the files separately or provide file IDs. Once processed, the information becomes searchable through Knowledge Graph queries.

## Examples

### Ingest company policies

The following example shows how to add documents to a Knowledge Graph. In this example, the user uploads one or more documents via the [**File Input** interface block](/components/fileinput), which sets the `@{files}` state variable to the user's uploaded files.

**Blueprint flow:**

1. **UI Trigger** → User initiates document ingestion with a button click. The **File Input** interface block set the `@{files}` state variable to the user's uploaded file.
2. **Add to Knowledge Graph** → Ingest and add the files from the `@{files}` state variable to the Knowledge Graph.
3. **Set state** → Store whether the files were successfully ingested so the UI can display feedback to the user.

**Add to Knowledge Graph block configuration:**

* **Graph Id:** `Support KB`
* **Files:** `@{files}`

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Graph Id</td>
      <td>Graph Id</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The id for an existing knowledge graph. It has a UUID format.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Format: uuid
      </td>
    </tr>

    <tr>
      <td>Files</td>
      <td>Object</td>
      <td>-</td>

      <td>
        <code>\[]</code>
      </td>

      <td>A list of files to be uploaded and added to the knowledge graph. You can use files uploaded via the File input component or specify dictionaries with data, type and name.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>URLs</td>
      <td>Object</td>
      <td>-</td>

      <td>
        <code>\[]</code>
      </td>

      <td>A list of URLs to be added to the knowledge graph. Web content from these URLs will be indexed.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>Content was added to the knowledge graph.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error adding content to the knowledge graph.</td>
    </tr>
  </tbody>
</table>

The **Add to Knowledge Graph** block does not return any output.


# Add to state list
Source: https://dev.writer.com/blueprints/addtostatelist



Adds a new item to a list in the Agent's state. Useful for tracking multiple values over time.

<img alt="" />

## Overview

This block adds a string value to a list in the agent's state. It behaves differently depending on the state element's type:

* If the state element doesn't already exist, the block creates it and adds the value to it.
* If the state element already exists as a list, the block adds the value to the list.
* If the state element exists but isn't a list, the block fails.

<Warning>The **Add to state list** block only accepts string values. If you pass a value such as a dictionary, it will be converted to a string and added to the list.</Warning>

## Example

The following example shows an **Add to state list** block as part of a **For-each** workflow.

<img alt="" />

The **For-each loop** block iterates over a list of values and generates text for each value. Then, the **Add to state list** block adds the result of each iteration to the state list called `text_gen_results`.

On the initial run, the **Add to state list** block creates the state element and adds the first value to it. Every subsequent run, the block adds the next value to the list.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Link Variable</td>
      <td>Binding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>Set the variable here and use it across your agent.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The item was added to the list.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error adding the item to the list.</td>
    </tr>
  </tbody>
</table>


# AI Studio Agent
Source: https://dev.writer.com/blueprints/aistudioagent



Runs a Writer AI Studio agent app by ID.

<img alt="" />

## Overview

The **AI Studio Agent** block runs a [deployed no-code AI Studio agent](/no-code/introduction). Only agents with [text generation](/no-code/text-generation) and [research](/no-code/research) capabilities are supported.

Under `App Id`, you can select the no-code agent you'd like to run from a pre-populated list of your deployed no-code agents.

<Warning>No-code agents with [chat capabilities](/no-code/chat) are not supported with this blueprint block. See the [Agent Builder chatbot tutorial](/agent-builder/chatbot-tutorial) for an example of how to create a chatbot with Agent Builder.</Warning>

## Inputs

The **AI Studio Agent** block takes key-value pairs in the `App inputs` field. The inputs should match the inputs of the no-code agent you're running. The key is the name of the input and the value is the value you'd like to pass to the agent.

If the no-code agent is a research assistant, it takes a single input with the key `query`. The value is the query you'd like the agent to run.

<img alt="" />

### File and image inputs

If the input is a file or an image, you must upload the file to Writer first and then pass the ID to the agent. You can't pass the file or image directly to the agent.

See the [Upload, parse, and summarize PDFs](/agent-builder/summarize-pdfs#first-blueprint%3A-upload-file-to-writer-cloud) tutorial for more information on how to upload files to the Writer cloud.

If the no-code agent accepts URLs to a file or image as input, you can pass a URL to the agent as a value rather than uploading the file to the Writer cloud.

## Output

The **AI Studio Agent** block returns the output of the no-code agent as a string.

You can access the output of an **AI Studio Agent** block using the `@{result}` variable in the block that follows it in a blueprint.

## Example

The following example shows an **AI Studio Agent** block that runs a no-code AI Studio agent.

<img alt="" />

The no-code agent is an NDA review assistant that accepts a file input with the name `NDA` and runs a text generation workflow. It returns the analysis from the workflow in the `Output formatting` field.

<img alt="" />

In this example blueprint, the **AI Studio Agent** block provides a file ID for an uploaded file, which is stored in the agent's state as `nda_file_id`. The file must already be uploaded to the Writer cloud.

<img alt="" />

The **AI Studio Agent** block returns the output of the no-code agent and proceeds to the next block, which stores the `@{result}` in a state variable.

## Errors

If the AI Studio Agent block fails with the error `Failed to acquire proper response for completion from data`, ensure that your no-code agent in AI Studio returns a value in the `Output formatting` field and that you are passing the correct inputs to the agent.

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>App Id</td>
      <td>App Id</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The agent id can be found in the agent's URL. It has a UUID format.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Format: uuid
      </td>
    </tr>

    <tr>
      <td>App inputs</td>
      <td>Key-Value</td>
      <td>-</td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The agent ran successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error running the agent.</td>
    </tr>
  </tbody>
</table>

Access the output of an **AI Studio Agent** block using the `@{result}` variable in the block that follows it in a blueprint.


# API Trigger
Source: https://dev.writer.com/blueprints/apitrigger



Triggers an event via API call.

<img alt="" />

## Overview

The **API Trigger** block enables external systems to trigger your blueprint via HTTP API calls. Use it to integrate your agent with webhooks, external services, or other applications that need to programmatically execute your workflows.

Unlike the [**UI Trigger**](/blueprints/uitrigger) block that responds to user interactions in the interface, the **API Trigger** block accepts HTTP POST requests to trigger blueprint execution.

<Tip>
  You must deploy your agent before you can trigger a blueprint via API.
</Tip>

## How it works

1. **Agent and Blueprint IDs**: Each API trigger block is associated with a specific agent and blueprint ID that becomes part of the API endpoint URL
2. **HTTP endpoints**: Creates two endpoints:
   * A synchronous POST endpoint that accepts JSON payloads and returns Server-Sent Events (SSE)
   * An asynchronous POST endpoint that returns a job ID for polling results
3. **JSON input processing**: The HTTP request body becomes available as `@{result}` in your blueprint execution, with your data nested under the `inputs` key
4. **Return Value**: To return a value from your blueprint to the API caller, you include a [**Return Value**](/blueprints/returnvalue) block at the end of your blueprint workflow

### Blueprint structure requirements

When building blueprints that will be triggered via API, ensure your workflow ends with a **Return Value** block:

1. **Start with API Trigger**: Begin your blueprint with the API Trigger block
2. **Process your logic**: Add all the blocks needed for your workflow
3. **End with Return Value**: Always finish with a Return Value block to specify what gets returned to the API caller

**Example blueprint structure:**

```
API Trigger → [Your workflow blocks] → Return Value
```

## API endpoint

When you add an **API Trigger** block to your blueprint, it creates a public HTTP endpoint that you can use to trigger your blueprint. See [API Trigger usage guide](/agent-builder/api-trigger) for more details about the API endpoints and how to use them.

### Synchronous endpoint

```
POST https://api.writer.com/v1/agents/{agent_id}/blueprints/{blueprint_id}
```

### Asynchronous endpoint

```
POST https://api.writer.com/v1/agents/{agent_id}/blueprints/{blueprint_id}/jobs
```

### Authentication

All API calls require authentication using a Bearer token in the Authorization header. Learn how to [get your API key](/api-reference/api-keys).

```
Authorization: Bearer <your_api_key>
```

## Request format

Send a POST request with a JSON body containing the data you want to pass to your blueprint under the `inputs` key:

```json theme={null}
{
  "inputs": {
    "user_id": "123",
    "action": "process_order"
  }
}
```

This data is available in your blueprint as `@{result.inputs.user_id}` and `@{result.inputs.action}`.

<Warning>
  The `inputs` key is required for the JSON payload. If you don't include it, the API will return an error.
</Warning>

## Response format

The synchronous API returns Server-Sent Events (SSE) with real-time execution status:

```
data: {"status":"in progress","message":null}
data: {"status":"executing","message":"Executing blueprint..."}
data: {"delta":"We have placed the order"}
data: [DONE]
```

The asynchronous API returns a job ID that you can poll for status updates using the following endpoint:

```
GET https://api.writer.com/v1/agents/jobs/{job_id}
```

When the job is complete, the response from the asynchronous API contains the result from the **Return Value** block in the `artifact` field.

## Testing

You can test your API trigger using the **Run blueprint** button in the Agent Builder editor. The **Default result** field provides sample data for testing without making actual HTTP requests.

## Learn more

For comprehensive implementation details, examples, and troubleshooting, see the [API Trigger usage guide](/agent-builder/api-trigger).

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Blueprint ID</td>
      <td>Blueprint Id</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Default result</td>
      <td>Code</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The result that is used when the blueprint is triggered from the "Run blueprint" button</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Trigger</td>
      <td>-</td>
      <td>success</td>
      <td>-</td>
    </tr>
  </tbody>
</table>

The **API Trigger** block returns the input payload that was sent to the API endpoint.

When triggered via API, the HTTP request body becomes available as `@{result}` in your blueprint execution, with your data nested under the `inputs` key. For example, if you send `{"inputs": {"user_id": "123"}}`, you can access it in your blueprint using `@{result.inputs.user_id}`.


# Ask graph question
Source: https://dev.writer.com/blueprints/askgraphquestion



Asks a natural language question using one or more knowledge graphs and puts the result into a state variable.

<img alt="" />

## Overview

The **Ask graph question** block queries one or more Writer Knowledge Graphs to find relevant information and generate answers based on the stored data. Use it to create AI applications that can reference and retrieve information from your uploaded documents and structured data.

You can specify the question, the list of Knowledge Graphs to search, whether to use streaming or subqueries, and a state variable to store the answer in. The block searches through the Knowledge Graphs and returns a relevant response.

## Common use cases

* Building question-answering systems based on company documents
* Creating AI assistants that reference specific knowledge bases
* Creating compliance verification tools that check against policy documents
* Building research assistants that analyze academic papers or reports

## How it works

1. **Question**: Enter the natural language question you want to ask about the stored data.
2. **Graph Ids**: Specify one or more Knowledge Graphs to query from a list of available Graphs.
3. **Link Variable**: The name of the variable that stores the answer in the agent's state.
4. **Use streaming**: Choose whether to stream the answer as it is generated. If you choose `yes`, the answer will be streamed to the UI as it is generated. If you choose `no`, the answer will be generated and then returned to the agent once it is complete.
5. **Use subqueries**: Enable to allow the LLM to ask follow-up questions to the Knowledge Graph for improved answers.

The block searches the selected Knowledge Graph or Graphs for relevant information and generates an answer based on the stored documents and data.

## Examples

### Employee policy assistant

This example shows a complete workflow where employees can ask questions about company policies and receive accurate, up-to-date information.

**Interface:**

1. **Text input** → Employee submits a question and the text input stores it in a state variable called `question`
2. **Button** → Employee clicks a button to submit the question to the agent
3. **Text** → Employee sees the answer stored in the `policy_answer` state variable in a text block

**Blueprint Flow:**

1. **UI Trigger** → Employee submits policy question through form
2. **Ask graph question** → Searches company knowledge base
3. **Text generation** → Formats the answer for the employee with additional context
4. **Set state** → Stores response for display

**Block Configuration:**

* **Graph Ids:** `["Company Policies", "Employee Handbook"]`
* **Question:** `@{question}` (set from a text input in the UI)
* **Link Variable:** `policy_answer`
* **Use streaming:** `yes`
* **Use subqueries:** `yes`

This workflow ensures employees receive accurate, up-to-date policy information directly from company documents.

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Question</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>The natural language question to ask.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Use streaming</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <code>yes</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Link Variable</td>
      <td>Binding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>Set the variable here and use it across your agent.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Graph Ids</td>
      <td>Graph Ids</td>
      <td>-</td>

      <td>
        <code>""</code>
      </td>

      <td>IDs of the graphs to query.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Use subqueries</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <code>yes</code>
      </td>

      <td>Enables LLM to ask follow-up questions to the knowledge graph. This improves answers, but may be slower.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>Successfully streamed the answer.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error answering the question.</td>
    </tr>
  </tbody>
</table>

The **Ask graph question** block stores the answer to the question in a state variable that you set in the `Link Variable` field. Access the output by referencing the state variable you defined, or use the `@{result}` variable in the next block.


# Change page
Source: https://dev.writer.com/blueprints/changepage



Navigates the user to another page in the app. Requires a valid page key.

<img alt="" />

## Overview

The **Change page** block navigates the user to a different page in the application. Use it to control the flow of your app, redirect after actions, or guide users through multi-step processes.

You can specify the target page and optionally pass parameters.

## Common use cases

* Redirecting users after form submission
* Navigating between steps in a multi-step workflow
* Sending users to a confirmation or error page
* Guiding users through onboarding or tutorials

## How it works

1. **Page**: Enter the name or path of the page to navigate to. Each page needs a unique key to identify it in the blueprint.
2. **Parameters**: Optionally specify parameters to pass to the new page.

The block triggers a navigation event, sending the user to the specified page.

## Examples

### Post-submission redirect

**Interface:**

1. A page with text inputs for name and email, and a button to submit the form.
2. A second page with the key `confirmation_page`. It contains a message to confirm the form submission with more details about the form submission.

**Blueprint Flow:**

1. **UI Trigger** → User presses button to submit a form
2. **Change page** → Navigates to the page with the key `confirmation`

**Block Configuration:**

* **Page key:** `confirmation`

<img alt="" />

This workflow ensures users are directed to the confirmation page after submitting the form.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Page key</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The identifying key of the target page.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The page change was successful.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error changing the page.</td>
    </tr>
  </tbody>
</table>


# Chat reply
Source: https://dev.writer.com/blueprints/chatreply



Initializes conversations, adds messages, and generates replies.

<img alt="" />

## Overview

The **Chat reply** block is a comprehensive chat solution that can initialize conversations, add messages, and generate replies in a single block. It's designed to handle the complete chat workflow efficiently.

You can specify a conversation state variable to store the chat history, system prompt, message, and various configuration options. The block manages the conversation state and generates appropriate responses. You can also configure tools that the AI can use during the conversation, such as function tools and Knowledge Graphs.

## Common use cases

* Building complete chat applications
* Creating AI assistants with conversation management
* Implementing interactive workflows with chat capabilities
* Developing conversational interfaces with tool calling

## How it works

1. **Conversation Object**: A binding variable that stores the chat history and metadata. If not provided or empty, a new conversation will be created and stored in the state.
2. **System prompt**: Set the context or behavior for the AI. Can be left empty if conversation is already initialized in state.
3. **Message**: Enter the user's message as an object with `role` and `content` properties; for example, `{"role": "user", "content": "What are the best practices for using the product?"}`. If you want to pass the user's message from the Chatbot interface to the Chat reply block, you can use the `@{payload}` variable.
4. **Configuration**: Set model (default: `palmyra-x5`), temperature (0-1), and max tokens (1-16384).
5. **Use streaming**: Choose whether to stream responses as they're generated or wait for complete responses.
6. **Tools configuration**: Configure tools that the AI can use during the conversation:
   * **Function tools**: Define custom functions with parameters that the AI can call
   * **Graph tools**: Connect to knowledge graphs for enhanced responses
   * **Tool routing**: Control when and how tools are called during the conversation
   * **Tool responses**: Handle and process the results returned from tool executions
   * See [tool calling](/agent-builder/tool-calling) for more information on how to configure tools.

## Examples

### Customer support chatbot

This example shows a customer support chatbot that can handle conversations and route to human agents if needed.

**Interface:**

1. A [**Chatbot** block](/components/chatbot) that displays the chat interface.

**Blueprint flow:**

1. **UI Trigger** → Customer sends message through chat interface
2. **Chat reply** → Processes message and generates AI response
3. **Classification** → Determines if human agent is needed
4. **Conditional routing** → Routes to human agent via HTTP Request block if a human agent is needed. Otherwise, the workflow starts again with the next user message.

**Chat reply block configuration:**

* **Conversation Object:** `@{chat}`
* **System prompt:**

```
You are a helpful customer support assistant. Be friendly and professional. 

IMPORTANT: If you encounter any of the following situations, clearly indicate that you need to route to a human agent:
- Complex technical issues beyond your capabilities
- Requests for account modifications or sensitive information
- Complaints that require escalation
- Situations where you're unsure or need human judgment
- Requests for supervisor or manager assistance

When routing to human, use phrases like:
- "I need to connect you with a human agent for this request"
- "Let me escalate this to our support team"
- "This requires human assistance, let me transfer you"
- "I'll need to route this to a specialist"

For routine questions and simple requests, provide helpful responses directly.
```

* **Message:** `@{payload}`. This passes the user's message from the Chatbot interface to the Chat reply block.

<img alt="" />

This workflow provides automated customer support while ensuring complex issues go to human agents.

### Connect a chatbot to a Knowledge Graph

This example shows a chatbot that can answer questions about domain-specific knowledge within a Knowledge Graph.

**Interface:**

1. A [**Chatbot** block](/components/chatbot) that displays the chat interface.

**Blueprint flow:**

1. **UI Trigger** → Customer sends message through chat interface
2. **Chat reply** → Processes message and generates AI response. Configured with a Knowledge Graph tool to answer questions about the company's products and services.

**Chat reply block configuration:**

* **Conversation Object:** `@{chat}`
* **System prompt:**

```
You are a helpful customer support assistant. Be friendly and professional. You have access to information about the company's products and services.
```

* **Tools:**
  * **Tool type:** `Knowledge Graph`
  * **Tool name:** A name to help you identify the tool in the response.
  * **Graph id(s):** The list of Knowledge Graphs to use. You can select from a list of available Knowledge Graphs.

<img alt="" />

This workflow provides a chatbot that can answer questions about the company's products and services.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Conversation Object</td>
      <td>Binding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The variable that has your conversation object.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>System prompt</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <code>""</code>
      </td>

      <td>A system prompt to set the context for the conversation. Can be left empty if conversation is already initialized in state.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Message</td>
      <td>Object</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>A message object. Content can be text string or array of objects for multimodal (text + images) with X5 model.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Generate reply</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <code>yes</code>
      </td>

      <td>If set to 'yes', the block will generate a reply from the model after adding the message. If set to 'no', it will only add the message to the conversation.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Initial model</td>
      <td>Model Id</td>
      <td>-</td>

      <td>
        <code>palmyra-x5</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Initial temperature</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <code>0.7</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Range:
        0    to  2
      </td>
    </tr>

    <tr>
      <td>Initial max tokens</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <code>1024</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Range:
        1    to  16384
      </td>
    </tr>

    <tr>
      <td>Use streaming</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <code>yes</code>
      </td>

      <td>If set to 'yes', the block will stream the reply as it is generated. If set to 'no', it will wait for the entire reply to be generated before returning.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Tools</td>
      <td>Tools</td>
      <td>-</td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Tools</td>
      <td>tools</td>
      <td>dynamic</td>
      <td>Run associated tools.</td>
    </tr>

    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The reply was generated successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error generating the reply.</td>
    </tr>
  </tbody>
</table>

The `dynamic` end state means that the exact values of this end state change based on how you define the block.

The output of the **Chat reply** block is a string with the most recent response from the AI to the user's message. You can access the output in the next block with the `@{result}` variable.


# Classification
Source: https://dev.writer.com/blueprints/classification



Classifies text into predefined categories using AI. Useful for tagging and routing inputs.

<img alt="" />

## Overview

The **Classification** block classifies text into predefined categories using AI. Unlike blocks that have a single "success" path (such as the [Tool calling block](/blueprints/toolcalling)), the Classification block creates multiple execution paths based on your defined categories. The workflow continues down the specific path that matches the classification result, not through a single return value or success state.

This makes the Classification block ideal for:

* Routing workflows based on content type, sentiment, or intent
* Tagging content into predefined categories
* Branching logic where different categories trigger different actions

For example, if you define categories like `urgent`, `normal`, and `low_priority`, the Classification block analyzes the input text and continues execution down the corresponding path (`urgent`, `normal`, or `low_priority`).

**Key difference from other blocks**: the Classification block doesn't have a final `success` state that takes a return value. Instead, it creates separate execution paths for each category you define, and the workflow continues down the path that matches the classification result.

The `@{result}` variable from the **Classification** block contains the name of the selected category.

## Common use cases

* **Customer support routing**: Classify support tickets as "Technical", "Billing", or "General" to route them to appropriate teams
* **Content moderation**: Categorize user submissions as "Appropriate", "Needs Review", or "Inappropriate"
* **Sentiment analysis**: Classify feedback as "Positive", "Negative", or "Neutral"
* **Document organization**: Sort documents by type such as "Invoice", "Contract", or "Report"
* **Lead qualification**: Classify sales inquiries as "Hot Lead", "Warm Lead", or "Information Request"

## How it works

1. **Input text**: Provide the text you want to classify (often from user input or previous block results)
2. **Define categories**: Set up category names and descriptions that help the AI understand what each category represents
3. **AI classification**: The block analyzes the text and determines which category best fits
4. **Routing**: The workflow automatically follows the connection path that matches the chosen category

## Example

The following example shows a **Classification** block that analyzes customer reviews to determine their primary focus, then routes to different response workflows based on the classification.

<img alt="" />

In this example, a customer review is classified into one of five categories:

* **Packaging**: Issues with how the product was packaged or shipped
* **Pricing**: Concerns about cost, value, or billing
* **Quality**: Problems or praise related to product quality
* **Delivery**: Issues with shipping speed or delivery process
* **Empty**: Reviews that don't contain useful feedback

Based on the classification, the workflow routes to different **Text generation** blocks that create appropriate responses for each category.

### Setting up categories

When configuring the **Classification** block, you define categories as key-value pairs:

<img alt="" />

**Key**: The category name (this becomes the connection point name)\
**Value**: A description that helps the AI understand what belongs in this category

For the customer review example:

* **Quality**: "The review is about the quality of the product or service"
* **Pricing**: "The review discusses pricing concerns or satisfaction"
* **Delivery**: "The review relates to delivery time or issues"

### Using classification results

After classification, the workflow automatically follows the connection that matches the selected category. You can connect different blocks to each category to create specialized workflows.

The classification result is also available in subsequent blocks using `@{result}`, which contains the name of the selected category.

## Best practices

### Writing effective category descriptions

* **Be specific**: Include concrete examples of what should be classified in each category
* **Use clear language**: Avoid ambiguous terms that could apply to multiple categories
* **Consider edge cases**: Think about borderline cases and which category they should fall into
* **Test with real data**: Use actual examples from your use case to verify classifications are accurate

### Category naming

* **Use descriptive names**: Choose names that clearly indicate the category's purpose
* **Follow naming rules**: Category names should contain only letters, digits, underscores, and spaces
* **Keep it consistent**: Use a consistent naming convention across all categories
* **Avoid special characters**: Stick to alphanumeric characters for reliable connections

### Performance optimization

* **Provide context**: Use the "Additional context" field to give the AI more information for better classification
* **Handle edge cases**: Always include a "Other" or "Unknown" category for inputs that don't fit your main categories

### Adding context for better classification

Use the "Additional context" field to provide information that helps with classification:

```
Additional context: This is a product review for a high-end electronics item. Focus on technical aspects, build quality, and user experience when classifying.
```

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Text</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The text you want to classify.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Categories</td>
      <td>Key-Value</td>
      <td>-</td>

      <td>The keys should be the categories you want to classify the text into, for example 'valid' and 'invalid', and the values are the criteria for each category. Category names should contain only letters of the English alphabet, digits, underscores, and spaces.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Additional context</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>Any additional information that might help the AI in making the classification decision.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>-</td>
      <td>categories</td>
      <td>dynamic</td>
      <td>-</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error classifying the text.</td>
    </tr>
  </tbody>
</table>

The `dynamic` end state means that the exact values of this end state change based on how you define the block.

The output of a **Classification** block is a string that contains the classification of the input text. You can access the output of a **Classification** block using the `@{result}` variable in the block that follows it in a blueprint.


# For-each loop
Source: https://dev.writer.com/blueprints/for-eachloop



Loops through each item in a list to run the same logic.

<img alt="" />

## Overview

The **For-each loop** block iterates over a list or dictionary and executes a workflow for each item.

The following variables are available in the workflow when iterating with the **For-each loop** block:

* For a list of items (for example, `["a", "b", "c"]`):
  * `@{item}`: The current item in the list (for example, `"a"`, `"b"`, or `"c"`)
  * `@{itemId}`: The current item's index in the list, starting at 0 (for example, `0`, `1`, or `2`)
* For a dictionary of items (for example, `{"a": "apple", "b": "banana", "c": "cherry"}`):
  * `@{item}`: The current item's value in the dictionary (for example, `"apple"`, `"banana"`, or `"cherry"`)
  * `@{itemId}`: The current item's key in the dictionary (for example, "a", "b", or "c")

## Example

The following example shows a **For-each loop** block that loops over a list of file IDs and makes an HTTP request to the Writer API delete each file in the list by ID. It then sets a state variable to display a success message when it's done.

<img alt="" />

<Info>In this example, the list of file IDs is a hardcoded list of strings. In a more realistic example, the list of file IDs would be returned from a previous block or stored in state, rather than hardcoded. In that case, you could use the environment variable such as `@{payload}` or `@{state_element}` to access the list in the `Items` field.</Info>

The **For-each loop** block iterates over the list of items in the `Items` field and executes the blocks that follow for each item. To reference the current item in the loop, you can use the environment variable `@{item}`. To reference the current item's index in the list, you can use the environment variable `@{itemId}`.

The HTTP request in this example uses the `@{item}` environment variable to reference the current file ID in the HTTP request's `URL` field.

<img alt="" />

When the **For-each loop** block is done, it moves to the next block connected to the **Success** connection point. In this example, the next block is a **Set state** block that sets a state variable to display a success message when it's done.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Items</td>
      <td>Object</td>
      <td>Textarea</td>

      <td>
        <code>\[]</code>
      </td>

      <td>The item value will be passed in the execution environment. It will be available at @, and its ID at @. You can use either a list or a dictionary.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Prefix</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>If set, the item will be available at @ and the item id at @.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Loop</td>
      <td>-</td>
      <td>dynamic</td>
      <td>Connect the branch that you'd like to loop on. The branch plugged in here will be executed once per item.</td>
    </tr>

    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The branch referenced executed successfully for each item.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>The branch referenced has failed for at least one of the items.</td>
    </tr>
  </tbody>
</table>

The `dynamic` end state means that the exact values of this end state change based on how you define the block.


# HTTP Request
Source: https://dev.writer.com/blueprints/httprequest



Sends an HTTP request to an API endpoint. Used to fetch or send data.

<img alt="" />

## Overview

The **HTTP Request** block sends an HTTP request to an API endpoint. Use it to fetch data from external services, send data to APIs, or integrate with third-party systems.

You can configure the HTTP method, URL, headers, body, and body type (plain text or JSON). The block supports `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` methods.

## Common use cases

* Fetching data from a REST API
* Sending data to a webhook or external service
* Integrating with third-party tools
* Automating workflows that require external communication

## How it works

1. **Method**: Choose the HTTP method, for example, `GET`, `POST`.
2. **URL**: Enter the endpoint URL. Pass path parameters in the URL; for example, `https://api.openweathermap.org/data/2.5/weather?q=@{payload.city}&appid=@{vault.openweather_key}&units=metric`.
3. **Headers**: Optionally specify headers as key-value pairs. You can also use [secrets](/agent-builder/secrets) to access API keys and other sensitive values.
4. **Body type**: Choose whether the body you are sending is `text` or `JSON`.
5. **Body**: Enter the request body for `POST`, `PUT`, and `PATCH` requests.

The block sends the request and returns the response. If the request fails due to a connection error (for example, network timeout, DNS failure), it outputs a connection error. If the request receives an error response from the server (for example, `4xx` or `5xx` status code), it outputs a response error. You can access the error details in the next block using the `@{message}` variable.

## Examples

### External API integration

This example demonstrates making HTTP requests to an external API using a public API endpoint. It uses the [Open-Notify API](https://open-notify.org/), which provides real-time data about the International Space Station (ISS); it's free and requires no authentication, so you can use this example to test your workflow before you use it with another API.

**Blueprint flow:**

1. **UI Trigger** → Receives request to fetch ISS location
2. **HTTP Request** → Calls Open-Notify API to get current ISS position
3. **Text generation** → Creates location description based on the body of the response (`@{result.body}`), which contains the latitude and longitude of the ISS
4. **Set state** → Stores the result of the text generation block in state variable to display in the UI

**HTTP Request block configuration:**

* **Method:** GET
* **URL:** `http://api.open-notify.org/iss-now.json`
* **Headers:** None
* **Body type:** None
* **Body:** None

<img alt="" />

This workflow enables integration with external APIs to enhance your agent's capabilities.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Method</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <ul>
          <li>GET - GET</li>

          <li>POST - POST</li>

          <li>PUT - PUT</li>

          <li>PATCH - PATCH</li>

          <li>DELETE - DELETE</li>
        </ul>
      </td>

      <td>
        Allowed values: GET, POST, PUT, PATCH, DELETE,
      </td>
    </tr>

    <tr>
      <td>URL</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Headers</td>
      <td>Key-Value</td>
      <td>-</td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Body type</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <code>text</code>
      </td>

      <td>-</td>

      <td>
        <ul>
          <li>text - Plain text</li>

          <li>JSON - JSON</li>
        </ul>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Body</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The request was successful.</td>
    </tr>

    <tr>
      <td>Response error</td>
      <td>-</td>
      <td>error</td>
      <td>The connection was established successfully but an error response code was received or the response was invalid.</td>
    </tr>

    <tr>
      <td>Connection error</td>
      <td>-</td>
      <td>error</td>
      <td>The connection couldn't be established.</td>
    </tr>
  </tbody>
</table>


# If-Else
Source: https://dev.writer.com/blueprints/if-else



Evaluate custom Python code and redirect to 'true' or 'false' branch. Useful for conditional logic.

<img alt="" />

## Overview

The **If-Else** block evaluates a Python expression and routes workflow execution to either a **True** or **False** branch based on the result. Use it to create conditional logic in your blueprints, enabling different actions based on data conditions.

The expression must be a single Python expression that evaluates to a Boolean value. You can use variables, comparison operators, logical operators, and built-in Python functions in your expression.

## Common use cases

* Validating user input before processing
* Checking if data meets specific criteria
* Routing workflows based on configuration settings
* Implementing business rules and decision logic
* Verifying API responses or data availability

## How it works

1. **Expression**: Enter a Python expression that evaluates to `True` or `False`. The expression must be a single expression, not multiple statements.
2. **Evaluation**: The block evaluates the expression using the Python interpreter.
3. **Routing**: Based on the result:
   * If the expression evaluates to `True`, the workflow continues to the **True** branch
   * If the expression evaluates to `False`, the workflow continues to the **False** branch
   * If an error occurs during evaluation, the workflow continues to the **Error** branch

The block has access to [state variables](/agent-builder/state), [execution environment variables](/agent-builder/execution-environment), and standard Python operations.

### Expression examples

<CodeGroup>
  ```python Comparing values theme={null}
  state["age"] >= 18
  ```

  ```python Checking data presence theme={null}
  "email" in state and state["email"]
  ```

  ```python Multiple conditions theme={null}
  state["score"] > 80 and state["completed"] == True
  ```

  ```python Type checking theme={null}
  isinstance(payload, dict) and "user_id" in payload
  ```

  ```python String operations theme={null}
  state.get("status", "").lower() == "approved"
  ```
</CodeGroup>

## Examples

### Data quality validation

This example shows how to validate API response data before processing it further in the workflow.

**Blueprint Flow:**

1. **HTTP Request** → Fetches data from external API
2. **If-Else** → Validates response contains required fields
   3a. **True branch → Text generation** → Processes valid data
   3b. **False branch → Log message** → Records validation failure

**Block Configuration:**

* **Expression:** `isinstance(result, dict) and "data" in result and len(result["data"]) > 0`

This workflow ensures data quality before processing, preventing errors downstream.

### Score-based routing

This example demonstrates routing users to different workflows based on their performance score.

**Blueprint Flow:**

1. **UI Trigger** → User completes assessment
2. **Python code** → Calculates total score
3. **If-Else** → Checks if score meets passing threshold
   4a. **True branch → Text generation** → Generates certificate
   4b. **False branch → Text generation** → Generates improvement recommendations

**Block Configuration:**

* **Expression:** `state["total_score"] >= state["passing_threshold"]`

This workflow provides personalized responses based on user performance metrics.

<Tip>
  **Choosing the right block:**

  * **Use If-Else** for rule-based logic with exact conditions: numeric comparisons (`age >= 18`), data validation (`"email" in state`), checking Boolean flags, or evaluating structured data. Best when you know the exact criteria to evaluate.

  * **Use [Classification](/blueprints/classification)** for content-based routing: categorizing text by topic, sentiment, or intent. Best when you need AI to understand natural language meaning and route to multiple categories (for example, classifying support tickets as "Technical", "Billing", or "General").

  * **Use [Python code](/blueprints/pythoncode)** before If-Else for complex logic: when you need multiple statements, loops, or calculations before the conditional check. Store the result in state, then reference it in the If-Else expression.

  **Expression requirement:** The If-Else block accepts only a single Python expression that evaluates to a Boolean value, not multiple statements.
</Tip>

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Expression</td>
      <td>Eval</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The expression to be evaluated. Must be a single expression (no statements).</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>True</td>
      <td>-</td>
      <td>success</td>
      <td>The event handler execution for True.</td>
    </tr>

    <tr>
      <td>False</td>
      <td>-</td>
      <td>success</td>
      <td>The event handler execution for False.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>The expression evaluation failed.</td>
    </tr>
  </tbody>
</table>


# Key-Value Storage
Source: https://dev.writer.com/blueprints/key-valuestorage



Stores data between sessions. Uses unique keys (names) to identify the data. Keys can only contain alphanumeric characters, underscores, and hyphens.

<img alt="" />

## Overview

The **Key-Value Storage** block allows you to save data in the cloud between browser sessions and agent redeploys. Unlike [agent state](/agent-builder/state), which is session-based and temporary, Key-Value Storage provides persistent storage through browser refreshes, new sessions, and agent updates.

This block works with individual items or JSON-serializable objects such as lists, dictionaries, and other nested structures.

<Note>
  **Storage scope:** Key-Value Storage is **per agent**, not per user. All users of the same agent share the same storage namespace. If you need user-specific data, include a user identifier in your key names (for example, `user_preferences_{user_id}`) to ensure data isolation between users.
</Note>

## Common use cases

* Storing user preferences or settings that should persist across sessions
* Maintaining application data that needs to survive agent redeploys
* Storing configuration data or feature flags
* Persisting user progress or state in multi-step workflows

<Warning>
  Key-value storage is not a secure way to store sensitive data. If you need to store sensitive data, use [Vault](/agent-builder/secrets).
</Warning>

## How it works

The Key-Value Storage block provides four actions: [save](#save), [get](#get), [delete](#delete), and [list keys](#list-keys).

### Save

Specify a key (name) and a value you want to associate with it. If this key already exists, the existing value is updated. If it doesn't exist, it is created.

* **Key**: A unique identifier for your data. Keys can only contain alphanumeric characters, underscores, and hyphens.
* **Value type**: Choose how to interpret the value:
  * **Plain text**: Stores the value as a string.
  * **JSON**: Parses the value as JSON, allowing you to store objects, lists, and nested structures directly.
* **Value**: The data you want to store. You can enter plain text, use expressions like `@{state.username}`, or enter JSON directly when using JSON value type.

When you save a value, it's stored in the cloud and associated with your agent. The data persists even if the user closes their browser or you redeploy the agent.

<Note>
  To store structured data (objects, lists, nested structures), set Value type to **JSON** and enter your data directly in the Value field, or reference a state variable using `@{state.variable_name}`.
</Note>

### Get

Specify a key name to retrieve the data you've stored under that key. If the key exists, the block returns the stored value. If the key doesn't exist, the block raises an error that you can handle with error routing.

The retrieved value maintains its original structure. If you stored a complex object as JSON, you'll get back the same structure with all nested attributes accessible.

### Delete

Specify a key name to delete the data associated with that key. This permanently removes the key-value pair from storage. If you try to delete a key that doesn't exist, the block raises an error.

### List keys

Retrieve a list of all keys currently stored for your agent. This action doesn't require a key parameter, and returns an array of key names that you can use to iterate over stored data or check what data exists.

## Accessing nested data

When you store structured data (either by setting Value type to JSON or by referencing a state variable like `@{state.user_profile}`), you can access nested attributes directly in subsequent blocks using dot notation, similar to how you access [nested state variables](/agent-builder/state#nested-state-variables).

For example, if `state.user_profile` contains:

```json theme={null}
{
  "name": "John Doe",
  "preferences": {
    "theme": "dark",
    "language": "en"
  },
  "tasks": [
    {"id": 1, "title": "Task 1"},
    {"id": 2, "title": "Task 2"}
  ]
}
```

After saving this data (either by entering the JSON directly with Value type set to JSON, or by using `@{state.user_profile}` as the Value), you can retrieve it with the **Get** action and access nested values in subsequent blocks using dot notation:

* `@{result.name}` returns "John Doe"
* `@{result.preferences.theme}` returns "dark"
* `@{result.tasks.0.title}` returns "Task 1"

## Examples

### Caching computed results

This example shows how to cache expensive computations for reuse.

**Blueprint Flow:**

1. **UI Trigger** → User requests data analysis
2. **Key-Value Storage (Get)** → Attempts to retrieve cached result with key `analysis_result`
   * **Success path** → Display cached result
   * **Error path** → If cache doesn't exist:
     * **Python code** → Perform expensive computation
     * **Key-Value Storage (Save)** → Store result with key `analysis_result`
     * **Set state** → Display result to user

### Managing application configuration

This example demonstrates storing configuration data that survives agent redeploys.

**Blueprint Flow:**

1. **UI Trigger** → Admin updates configuration
2. **Key-Value Storage (Save)** → Stores configuration with key `app_config`
3. **Set state** → Confirms update

In other parts of the agent:

1. **Key-Value Storage (Get)** → Retrieves configuration with key `app_config`
2. **Python code** → Uses configuration values (accessible via `@{result}`) to control agent behavior

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Action</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <code>Save</code>
      </td>

      <td>-</td>

      <td>
        <ul>
          <li>Save - Save</li>

          <li>Get - Get</li>

          <li>Delete - Delete</li>

          <li>List keys - List keys</li>
        </ul>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Key</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value type</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <code>text</code>
      </td>

      <td>-</td>

      <td>
        <ul>
          <li>text - Plain text</li>

          <li>JSON - JSON</li>
        </ul>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The request was successful.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>The request wasn't successful.</td>
    </tr>
  </tbody>
</table>


# Log message
Source: https://dev.writer.com/blueprints/logmessage



Prints a message to the console for debugging or monitoring app flow.

<img alt="" />

## Overview

The **Log message** block writes a message to the workflow log. Use it to record information, debug workflows, or track the progress of your blueprint execution.

You can specify any message, including variables or state values, to help with troubleshooting or monitoring.

To view the logs, click the **Log** tab in the Agent Builder editor.

<img alt="" />

## Common use cases

* Debugging workflow execution by logging variable values
* Tracking the progress of a workflow
* Recording errors or important events
* Auditing workflow steps for compliance

## How it works

1. **Message**: Enter the message to log. You can log variables using the `@{variable_name}` syntax. For example, to log the value of the `@{payload}` variable, you can use the message `"Payload: @{payload}"`.
2. **Type**: Choose the type of message to log. You can choose from the following options:
   * **info**: Log a message with information.
   * **error**: Log a message with an error.

The block writes the message to the log. The message is available in the workflow execution logs for review.

## Examples

### Debug workflow execution

This example shows how to add logging throughout a workflow that processes multiple resumes from job applicants. The workflow extracts key information from each resume PDF and generates a summary report. The log message block is used to record the progress of the workflow.

**Blueprint Flow:**

1. **UI Trigger** → HR manager uploads multiple resumes for processing
2. **For-each loop** → Processes document sections
3. **Log message** → Records section completion and confirms parsing success
4. **Parse PDF** → Parses the PDF file
5. **Text generation** → Assembles final document

**Block Configuration:**

* **Message:** `Processing resume for @{itemId}.`
* **Type:** `info`

<img alt="" />

The logs display the progress of the workflow as it processes each resume. You can see the logs in the **Log** tab of the Agent Builder editor.

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Type</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <code>info</code>
      </td>

      <td>-</td>

      <td>
        <ul>
          <li>info - Info</li>

          <li>error - Error</li>
        </ul>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Message</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The message was logged successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error logging the message.</td>
    </tr>
  </tbody>
</table>

The return value of the **Log message** block is the message that was logged. You can access the return value in the next block in the workflow using the `@{result}` variable.


# Parse JSON
Source: https://dev.writer.com/blueprints/parsejson



Converts a JSON string into an object, so you can work with it in your logic.

<img alt="" />

## Overview

The **Parse JSON** block transforms a JSON-formatted string into a structured object that you can work with programmatically. For example, if you have a string like `'{"name": "John", "age": 30}'`, this block converts it into an object where you can access values like `object.name` or `object.age`.

This block is essential when you need to extract and work with data from:

* API responses that return JSON strings
* Form submissions containing JSON data
* Configuration files stored as JSON
* Any text that follows JSON format

The parsed object lets you:

* Access nested values using dot notation (for example, `user.address.city`)
* Iterate through arrays of data
* Pass structured data to other blocks
* Validate and transform the data structure

## Common use cases

* Parsing API responses
* Converting user input into structured data
* Extracting values from JSON payloads
* Validating JSON format before further processing

## How it works

1. **JSON string**: Enter the JSON string to parse. You can use variables or state values.

The block parses the string and outputs the resulting object. If the string is not valid JSON, the block raises an error.

## Examples

### User order form processing

This example shows why parsing JSON is essential for conditional logic and data extraction.

**Scenario:** A user submits an order form with JSON data that looks like this:

```json theme={null}
{
  "customer": {
    "name": "Sarah Johnson",
    "email": "sarah@example.com",
    "tier": "premium"
  },
  "items": [
    {"product": "Laptop", "price": 1299.99, "quantity": 1},
    {"product": "Mouse", "price": 29.99, "quantity": 2}
  ],
  "total": 1359.97,
  "shipping_method": "express"
}
```

**Without Parse JSON (problematic):**

* Text generation block sees: `"{"customer": {"name": "Sarah Johnson"...`
* Can't access specific values like customer name or total
* Can't apply conditional logic based on order value
* Can't calculate discounts for premium customers

**With Parse JSON (powerful):**

1. **UI Trigger** → User submits order form
2. **Parse JSON** → Converts form data to structured object
3. **Set state** → Store parsed data in a state variable called `parsed_order`
4. **Classification** → Check if `@{parsed_order.total}` >= 1000 for free shipping
5. **Text generation** → Use `@{parsed_order.customer.name}` and `@{parsed_order.total}` in response

**Block Configuration:**

* **JSON string:** `@{order}` (the name of the state variable that contains the JSON string from the interface)

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Plain text</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The request was successful.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>The text provided couldn't be parsed.</td>
    </tr>
  </tbody>
</table>

The output of the **Parse JSON** block is a structured object. You can use this object in the following block with the `@{result}` variable.

To access the values of the object in other blueprint blocks, you can use dot notation. For example, if the object looks like this:

```json theme={null}
{
  "customer": {
    "name": "Sarah Johnson",
    "email": "sarah@example.com",
    "tier": "premium"
  }
}
```

`@{result.customer.name}` will return the value of the `name` property of the `customer` object.


# Parse PDF tool
Source: https://dev.writer.com/blueprints/parsepdftool



Uses Writer API to extract the text content of a PDF file stored in Writer cloud.

<img alt="" />

## Overview

The **Parse PDF tool** block extracts text content from PDF files stored in the Writer cloud. Use it to convert PDF documents into searchable, analyzable text that can be used in AI workflows, content analysis, or data extraction processes.

## Common use cases

* Extracting text from PDF documents for analysis
* Converting PDF reports into searchable content
* Processing PDF forms and documents
* Building document processing workflows

## How it works

1. **PDF input**: Provide a PDF file UUID from the Writer cloud. The file must be uploaded to Writer first; you can upload a file first using the "Add files to Writer Cloud" block. You can also use files uploaded via the API, no-code agents, or the Writer cloud UI.
2. **Content extraction**: The block extracts text content from the PDF using the Writer API.
3. **Format selection**: Choose to enable markdown formatting for the extracted content.
4. **Text processing**: Converts PDF content into the selected format and returns it as a string.

The block handles PDF parsing, text extraction, and formatting. The extracted text maintains the document's structure.

### When to enable markdown formatting

You can choose to parse the PDF content as plain text or markdown. The default is plain text.

**When to use markdown:**

* Preserving document structure and hierarchy
* Maintaining formatting for readability
* Working with documents that have headings, lists, or emphasis
* Creating summaries that maintain document organization

**When to use plain text:**

* Simple text analysis and processing
* When formatting isn't relevant to your workflow
* Extracting raw text for AI processing
* Reducing complexity in downstream processing

## Examples

### Document analysis workflow

This example shows how to extract and analyze text from PDF documents in a complete workflow.

**Blueprint Flow:**

1. **UI Trigger** → User uploads PDF document
2. **Add files to Writer Cloud** → Uploads PDF document to Writer cloud
3. **Parse PDF tool** → Extracts text from PDF document
4. **Text generation** → Creates analysis and summary
5. **Set state** → Stores analysis for user review

**Block Configuration:**

* **File:** `@{result.0.id}`. This variable is the UUID of the file in Writer cloud. The `Add files to Writer Cloud` block returns a list of file objects that were uploaded to Writer cloud. To process multiple files, you can use the [**For each loop** block](/blueprints/for-eachloop) to iterate over the list of files.
* **Enable markdown:** `disabled`. Extracts content as plain text, because the formatting isn't relevant to the text generation task.

<Warning>The Parse PDF tool only accepts file UUID from the Writer cloud. Files must be uploaded first using the "Add files to Writer Cloud" block or File input component.</Warning>

<img alt="" />

This workflow enables automated document analysis and text extraction for research and content processing.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>File</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <code>""</code>
      </td>

      <td>UUID of a file object in Files API.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Format: uuid
      </td>
    </tr>

    <tr>
      <td>Enable markdown</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <code>yes</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The PDF was parsed successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error parsing the PDF.</td>
    </tr>
  </tbody>
</table>

The **Parse PDF tool** block returns the extracted text content as a string. Access the output using the `@{result}` variable in the block that follows it in a blueprint.


# Python code
Source: https://dev.writer.com/blueprints/pythoncode



Runs custom Python code. Useful for logic not covered by existing blocks.

<img alt="" />

## Overview

The Python code block allows you to write Python code to extend your blueprint's functionality. Use it to perform custom data processing, calculations, API integrations, or any other logic that requires Python programming capabilities.

## Common use cases

* Custom data processing and transformations
* Complex calculations and mathematical operations
* API integrations with external services
* File manipulation and data parsing

## How it works

1. **Code execution**: Write Python code in the block's code editor.
2. **Variable access**: Access [state variables](/agent-builder/state), [environment variables](/agent-builder/execution-environment), and [preinstalled libraries](/agent-builder/python-libraries).
3. **Output generation**: Use `set_output()` to return values to subsequent blocks.
4. **Error handling**: Handle exceptions and provide meaningful error messages. If an error occurs, access the error message in the next block using `@{message}`.
5. **Error exit condition**: Raise an `Exception` to trigger the **Error** exit condition and route to error handling blocks.

The block provides a full Python environment with access to [state variables](/agent-builder/state), [execution environment variables](/agent-builder/execution-environment), and [preinstalled libraries](/agent-builder/python-libraries).

## Examples

### Calculate average from list

This example shows how to return a value from the Python code block. It calculates the average of a list of numbers and returns the result to the next block by using the `set_output` function.

**Blueprint flow:**

1. **UI Trigger** → User provides list of numbers
2. **Python code** → Calculates average of numbers
3. **Set state** → Stores result in state variable

```python theme={null}
def calculate_mean(numbers):
    return sum(numbers) / len(numbers)

average = calculate_mean(state["numbers"])
set_output(average)
```

<img alt="" />

### Error handling with exceptions

You can trigger the **Error** exit condition by raising an `Exception` in your Python code. This allows you to route to error handling blocks when specific conditions are met.

**Example:**

```python theme={null}
# Validate user input and raise exception if invalid
user_age = state.get("age", 0)

if user_age < 0 or user_age > 120:
    raise Exception("Invalid age: Age must be between 0 and 120")

if not state.get("email"):
    raise Exception("Missing required field: Email address")

# Process valid data
set_output({"status": "valid", "age": user_age})
```

When an `Exception` occurs, the Python code block exits with the **Error** condition, allowing you to route to error handling blocks in your blueprint. You can access the error message in the next block by referencing the `@{message}` variable.

## Available variables and libraries

Python code block can access the following global variables and libraries:

* [State variables](/agent-builder/state)
* [Execution environment variables](/agent-builder/execution-environment)
* [Preinstalled Python libraries](/agent-builder/python-libraries)
* Functions you've defined in your `main.py` file

### Return values from the Python code block

To return a value from a Python code block and store it in the `result` execution environment variable, you must use the `set_output` function.

Anything that runs in the block but is not returned in the `set_output` function does not get passed to the next block.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Code</td>
      <td>Code</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>The code to be executed.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The code executed successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error executing the code.</td>
    </tr>
  </tbody>
</table>

Access the output of a **Python code** block using the `@{result}` variable in the block that follows it in a blueprint. The output contains whatever value was passed to the `set_output()` function.

Access the error message of a **Python code** block using the `@{message}` variable in the block that follows it in a blueprint. The error message contains whatever value was passed to the `raise Exception` function.


# Return value
Source: https://dev.writer.com/blueprints/returnvalue



Returns a value from this block to the caller. Use to pass results to another blueprint.

<img alt="" />

## Overview

The **Return value** block allows you to chain multiple steps together to perform certain actions, and return a single value at the end of the chain. You can use it in the following scenarios:

* To return a value in [tool calling](/agent-builder/tool-calling)
* To return a value when a blueprint is invoked with the [**Run blueprint** block](/blueprints/runblueprint)

It passes the value specified in the **Value** field at the end of a chain of blocks.

### Return a value in tool calling

When an **Tool calling** block decides to run a tool, the tool can include any number of blocks and return a final value to the **Tool calling** block. Use the **Return value** block to define what the final return value is.

If you do not use the **Return value** block, the tool call executes the blocks that follow it but doesn't return a value to the **Tool calling** block.

<img alt="" />

In the example above, the two **Return value** blocks provide the values of the "Call shipping API" block and the "Call order status API" HTTP Request blocks to the **Tool calling** block.

Without the **Return value** blocks, the "Call shipping API" and "Call order status API" blocks would execute, but their results would not be returned to the **Tool calling** block.

### Return a value from a blueprint

Use the **Return value** block to pass results to another blueprint.

<img alt="" />

In the example above, the **Return value** block follows the **Text generation** block. It sets the results from the text generation block as the final return value of the blueprint. The blueprint passes this value when it is invoked with the **Run blueprint** block.

Without the **Return value** block, the blueprint would run and the **Text generation** block would execute, but its result would not be returned to the block that ran the blueprint.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Value</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The value was returned successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error returning the value.</td>
    </tr>
  </tbody>
</table>


# Run blueprint
Source: https://dev.writer.com/blueprints/runblueprint



Starts another blueprint by key. Useful for breaking logic into smaller, reusable parts.

<img alt="" />

## Overview

The **Run blueprint** block allows you to call another blueprint from within your current workflow. This enables modular, reusable logic and lets you break complex workflows into smaller, manageable pieces.

You can specify the blueprint to run and pass input parameters. The output of the called blueprint is available for use in subsequent blocks.

## Common use cases

* Reusing logic across multiple workflows
* Breaking up large blueprints into smaller, maintainable components
* Creating shared utility blueprints
* Running conditional or dynamic workflows

## How it works

1. **Blueprint Key**: Select the blueprint to run by its key. The blueprint must have a key set in its settings. If the blueprint doesn't have a key, it does not show in the list of available blueprints.
2. **Payload**: Provide input data as text that will be available as `@{payload}` in the called blueprint. The payload is treated as plain text.

The block executes the specified blueprint and returns any output that the called blueprint returns with the **Return value** block.

<Tip>
  **No trigger required:** Blueprints called with the Run blueprint block don't need a UI trigger or any other trigger. They start execution immediately when called and can access the payload data you pass in.
</Tip>

## Examples

### Document processing workflow

This example demonstrates a document processing system that handles different file types using specialized blueprints.

**Blueprint Flow:**

1. **UI Trigger** → User uploads document through file input
2. **Classification** → Identifies document type (invoice, resume, or contract)
3. **Conditional routing** → Routes to appropriate processing blueprint
4. **Run blueprint** → Executes specialized document processor
5. **Set state** → Stores the processing result to display in the UI

**Block Configuration:**
For invoice processing:

* **Blueprint Key:** `invoice_processor`
* **Payload:** `@{file_data}`

**Called Blueprint Access:**
In the `invoice_processor` blueprint, you can access the passed data using `@{payload}`. Then you can return a value from the blueprint using the **Return value** block.

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Blueprint Key</td>
      <td>Blueprint Key</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Format: writer#blueprintKey
      </td>
    </tr>

    <tr>
      <td>Payload</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>The value specified will be available using the template syntax, e.g. @.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The request was successful.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>The blueprint execution failed.</td>
    </tr>
  </tbody>
</table>

The output of the **Run blueprint** block is the return value from the called blueprint. This is typically the result of the last block in the called blueprint, or a specific return value if the called blueprint has a **Return value** block. You can access this output in subsequent blocks using `@{result}`.


# Set state
Source: https://dev.writer.com/blueprints/setstate



Stores a value in the Agent's state. Use to remember data across steps.

<img alt="" />

## Overview

The **Set state** block is a core building block that connects information between different parts of your blueprint, interface, and code. It acts as a bridge, allowing data to flow from one part of your workflow to another - whether that's from blueprint to interface, between different blueprint blocks, from interface back to blueprint, or to and from custom Python code.

You can specify the variable name, value, and value type (plain text or JSON). The value is then available in subsequent blocks via the state variable.

## Common use cases

* Storing user input or intermediate results
* Passing data between blocks in a workflow
* Saving API responses or generated content for later use
* Managing workflow state or flags
* Storing block results to display in interface components
* Persisting data between interface updates and refreshes

## How it works

1. **Link Variable**: Specify the name of the state variable to set.
2. **Value type**: Choose whether the value is plain text or JSON. The default if not specified is plain text.
3. **Value**: Enter the value to store.

The block saves the value in the specified state variable. You can reference this variable in later blocks using the state syntax, for example, `@{state.variable_name}`.

## Examples

### Text generation workflow

This example shows how to generate text and display it in the interface using state management.

**Blueprint Flow:**

1. **UI Trigger** → User clicks "Generate" button
2. **Text generation** → Creates content based on prompt
3. **Set state** → Stores generated text

**Interface** → Has a **Text** component that displays the text from the state variable

**Block Configuration:**

* **Link Variable:** `generated_text`
* **Value type**: `text`. The default if not specified is plain text.
* **Value:** `@{result}`. This variable is the text generated by the text generation block that precedes the **Set state** block.

<img alt="" />

This workflow enables automated text generation and display in the interface.

### Form submission workflow with JSON value type

This example shows how to package multiple form field responses into a single state variable. It's useful when collecting and storing related data from interface forms.

**Blueprint Flow:**

1. **UI Trigger** → User submits a registration form. All the form fields have Link Variables set to the name of the field.
2. **Set state** → Stores all form fields as JSON

**Block Configuration:**

* **Link Variable:** `form_data`
* **Value type**: `JSON`
* **Value:**
  ```json theme={null}
  {
    "firstName": @{firstName},
    "lastName": @{lastName},
    "email": @{email},
    "phoneNumber": @{phoneNumber},
    "address": {
      "street": @{address.street},
      "city": @{address.city},
      "state": @{address.state},
      "zipCode": @{address.zipCode}
    },
    "preferences": {
      "newsletter": @{preferences.newsletter},
      "notifications": @{preferences.notifications}
    }
  }
  ```

<img alt="" />

To access specific fields of the JSON data, use the state syntax, for example `@{form_data.firstName}` or `@{form_data.address.city}`.

<Tip>
  To access elements of the state variable using Python, treat it as a dictionary. For example, `@{state["form_data"]["firstName"]}` or `@{state["form_data"]["address"]["city"]}`.
</Tip>

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Link Variable</td>
      <td>Binding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>Set the variable here and use it across your agent.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value type</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <code>text</code>
      </td>

      <td>-</td>

      <td>
        <ul>
          <li>text - Plain text</li>

          <li>JSON - JSON</li>
        </ul>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The state was updated successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error updating the state.</td>
    </tr>
  </tbody>
</table>

The **Set state** block returns the value that was set in the state variable. You can access this value in the following block as `@{result}`, or you can use the state syntax to access the value in the state variable, for example `@{form_data}`.


# Structured output
Source: https://dev.writer.com/blueprints/structuredoutput



Defines a JSON response format that the agent will use to structure its output.

<img alt="" />

## Overview

The **Structured Output** block allows you to define a JSON response format that the AI model uses to structure its output. This is useful when you need the model to return data in a specific schema, such as for downstream processing, API responses, reliable data extraction, or integrations. The block returns the JSON object that matches the schema.

You can specify a prompt describing the data you want, and provide a [JSON Schema](https://json-schema.org/) to enforce the structure of the output. The model attempts to generate a response that matches the schema.

## Common use cases

* Extracting structured data from unstructured text; for example, extracting entities, tables, or key-value pairs
* Generating API responses in a specific format
* Creating objects for further automation or integration
* Enforcing data validation on AI-generated output

## How it works

1. **Prompt**: Describe the data you want the model to generate.
2. **JSON Schema**: Define the required structure for the output. See [JSON Schema](https://json-schema.org/) for more information about how to write a JSON Schema.
3. **Model**: Select the model to use for generation.
4. **Max output tokens**: Set the maximum length of the output.

The block sends the prompt and schema to the model, which returns a JSON object matching the schema. If the output isn't parsable as JSON or doesn't match the schema, the block raises an error.

## Examples

### Data extraction from documents

This example shows how to extract structured data from unstructured documents using AI.

**Blueprint Flow:**

1. **UI Trigger** → User selects a document from the interface
2. **Parse PDF tool** → Extracts text from document. Note: the document must already be uploaded to the Writer cloud.
3. **Structured output** → Extracts specific data fields
4. **HTTP Request** → Sends structured data to external API

**Block Configuration:**

* **Prompt:** "Extract the following information from the document. Document content: @. Return the data in the following format: company name, contact person, email, phone number, and address."

* **JSON Schema:**

  ```json theme={null}
  {
    "type": "object",
    "properties": {
      "company_name": {"type": "string"},
      "contact_person": {"type": "string"},
      "title": {"type": "string"},
      "email": {"type": "string", "format": "email"},
      "phone": {"type": "string"},
      "address": {"type": "string"}
    },
    "required": ["company_name", "contact_person", "email"],
    "additionalProperties": false
  }
  ```

* **Model:** `palmyra-x5`

This workflow enables automated extraction of structured data from unstructured documents.

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Prompt</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>Description of a JSON object to be created.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Model</td>
      <td>Model Id</td>
      <td>-</td>

      <td>
        <code>palmyra-x5</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>JSON Schema</td>
      <td>JSON</td>
      <td>-</td>

      <td>JSON schema that defines the structure of the response. For example, `{"type": "object", "properties": {...}, "required": [...]}`.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Max output tokens</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <code>1024</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Range:
        1    to  16384
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The structured output was generated.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error generating the output.</td>
    </tr>
  </tbody>
</table>

The **Structured Output** block returns the JSON object that matches the schema. You can access this value in the following block as `@{result}`.


# Text generation
Source: https://dev.writer.com/blueprints/textgeneration



Generates text using a Writer model. Use for completions, summaries, or creative writing.

<img alt="" />

## Overview

The **Text generation** block generates text using a Palmyra model. Use it for text completions, summaries, creative writing, and more. You provide a prompt, and the model generates a response based on your instructions.

You can control the creativity of the output using the temperature setting, and limit the length with the max output tokens field.

## Common use cases

* Generating summaries or explanations
* Creative writing such as stories, marketing copy, and abstracts
* Expanding or rewriting text
* Answering questions based on a prompt

## How it works

1. **Prompt**: Enter the text or instructions for the model.
2. **Model**: Select the model to use for generation. Learn more about [the suite of Palmyra models](/home/models).
3. **Temperature**: Adjusts randomness and creativity. A higher temperature value like 0.7-1.0 makes the output more creative, while a lower temperature value like 0.0-0.3 makes it more deterministic.
4. **Max output tokens**: Sets the maximum length of the generated text.

The block sends the prompt and settings to the model, which returns the generated text as output.

## Examples

### Content creation workflow

This example shows a marketing workflow where user input is transformed into professional marketing copy.

**Blueprint Flow:**

1. **UI Trigger** → Marketing team submits product details through form
2. **Text generation** → Creates product description based on input
3. **Set state** → Stores generated content for review and approval in the interface

**Block Configuration:**

* **Prompt:** "Create a compelling product description for a @ that highlights its @. Target audience: @. Tone: professional but approachable."
* **Model:** `palmyra-x5`
* **Temperature:** `0.7`

<img alt="" />

This workflow automates content creation while ensuring appropriate tone and messaging for different marketing channels.

### Customer support response system

This example demonstrates an automated support system that generates personalized responses based on customer inquiries.

**Blueprint Flow:**

1. **UI Trigger** → Customer submits support ticket
2. **Classification** → Categorizes the issue type
3. **Text generation** → Creates personalized response based on category
4. **Set state** → Stores response for customer to display in the interface

**Block Configuration:**

* **Prompt:** "Generate a helpful and empathetic response for a customer unable to log into their @. Address common login troubleshooting steps like password reset, browser cache clearing, and two-factor authentication issues. Include links to our password reset tool and support documentation. If these steps don't resolve the issue, provide instructions for escalating to our technical support team."
* **Model:** `palmyra-x5`
* **Temperature:** `0.4`

<img alt="" />

This workflow provides automated, personalized customer support responses while maintaining human-like empathy and helpfulness.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Prompt</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Model</td>
      <td>Model Id</td>
      <td>-</td>

      <td>
        <code>palmyra-x5</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Temperature</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <code>0.7</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Range:
        0    to  1
      </td>
    </tr>

    <tr>
      <td>Max output tokens</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <code>1024</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        Range:
        1    to  16384
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>Text was generated successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error generating text.</td>
    </tr>
  </tbody>
</table>

The **Text generation** block returns the generated text as output. You can access this value in the following block as `@{result}`.


# Tool calling
Source: https://dev.writer.com/blueprints/toolcalling



Connects the Agent to external tools to complete tasks it cannot handle directly.

<img alt="" />

## Overview

The **Tool calling** block enables your agent to interact with external systems, APIs, and custom functions to gather information, perform actions, or execute complex workflows that extend beyond the model's built-in capabilities.

When the tool calling block executes, the AI model analyzes the prompt and automatically determines which tools to use, when to use them, and how to combine the results to complete the requested task. The model can make multiple tool calls in sequence and use the results from one tool to inform subsequent tool calls.

## How tool calling works

* **Tool selection**: The model analyzes your prompt and determines which available tools are needed
* **Parameter extraction**: The model extracts the necessary parameters from the prompt and context to call each tool
* **Sequential execution**: Tools are called in the optimal order, with results from previous tools informing subsequent calls
* **Result synthesis**: The model combines all tool results with its own knowledge to generate the final response

To learn more about how tool calling works, see the [Tool calling introduction](/agent-builder/tool-calling-intro) documentation.

## Tool types

The tool calling block supports two types of tools:

* **Function tools**: Custom workflows you build within the blueprint using other blocks
* **Built-in tools**: Pre-configured integrations with external services. Currently only [Knowledge Graphs](/home/knowledge-graph) are supported.

### Function tools

Function tools allow you to define custom logic using other blueprint blocks. Each function tool requires:

* **Tool name**: A unique identifier for the tool
* **Description**: Clear explanation of what the tool does and when to use it
* **Parameters**: Input fields the model should provide when calling the tool
* **Implementation**: The workflow of blocks that execute when the tool is called

<Warning>
  When you implement the chain of blocks that make up a function tool, you must use include a [Return value](/blueprints/returnvalue) block at the end of the chain to return a value to back to the model.
</Warning>

The model uses the tool name and description to determine when to call the tool, and uses the parameter definitions to extract the correct values from the prompt context.

#### Tool definitions

The tool definition for a `Function` tool follows the [JSON Schema](https://json-schema.org/) format. You provide the definition when adding the tool to the **Tool calling** block.

<img alt="" />

The definition has the following structure:

| Parameter               | Type   | Description                                                                                                                      |
| ----------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `name`                  | string | The name of the tool                                                                                                             |
| `description`           | string | A description of what the tool does and when the model should use it                                                             |
| `parameters`            | object | An object containing the tool's input parameters                                                                                 |
| `parameters.type`       | string | The type of the parameter, which is `object` for a JSON schema                                                                   |
| `parameters.properties` | object | An object containing the tool's parameters in the form of a [JSON schema](https://json-schema.org/). See below for more details. |
| `parameters.required`   | array  | An array of the tool's required parameters                                                                                       |

#### Example tool definition

Below is an example of a tool definition that gets the details of a package's shipping status. It takes a tracking number and a carrier name as input parameters and returns the shipping status of the package.

```json theme={null}
{
  "name": "check_shipping_status",
  "description": "Gets real-time shipping and tracking information for a package",
  "parameters": {
    "type": "object",
    "properties": {
      "tracking_number": {
        "type": "string",
        "description": "The shipping carrier's tracking number"
      },
      "carrier": {
        "type": "string",
        "enum": ["fedex", "ups", "usps", "dhl"],
        "description": "Shipping carrier name"
      }
    },
    "required": ["tracking_number", "carrier"]
  }
}
```

### Knowledge Graph tools

Knowledge Graph tools allow you to connect to one or more [Knowledge Graph](/home/knowledge-graph) to get information about a specific topic.

To add a Knowledge Graph tool, select the Knowledge Graph(s) you want to use from the dropdown of available Knowledge Graphs.

<img alt="" />

## Prompt engineering for tool calling

While AI models are highly intelligent and can reason about complex tasks, a well-structured prompt is essential for effective tool calling. The prompt serves as a blueprint that clearly defines what tools are available, guides the model through the decision-making workflow, and specifies the expected output format. Without this guidance, the model might miss important steps, use tools inefficiently, or produce results that don't match your requirements.

### Key elements of effective tool calling prompts:

* **Clear tool descriptions**: Explain what each tool does and when to use it
* **Decision logic**: Provide step-by-step workflow guidance
* **Output format**: Specify the structure and format of the expected response
* **Context handling**: Include relevant background information and constraints

## Example

Below is an example of a Tool calling block that is connected to multiple tools that run Python code or call external APIs.

<img alt="" />

For a complete walkthrough of building an agent with tool calling, see the [Tool calling with external APIs](/agent-builder/tool-calling-tutorial) tutorial.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Prompt</td>
      <td>Text</td>
      <td>Textarea</td>

      <td>
        <span>-</span>
      </td>

      <td>The task that needs to be carried out.</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Model</td>
      <td>Model Id</td>
      <td>-</td>

      <td>
        <code>palmyra-x5</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Max iterations</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <code>10</code>
      </td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Tools</td>
      <td>Tools</td>
      <td>-</td>

      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Tools</td>
      <td>tools</td>
      <td>dynamic</td>
      <td>Run associated tools.</td>
    </tr>

    <tr>
      <td>Success</td>
      <td>-</td>
      <td>success</td>
      <td>The task was completed successfully.</td>
    </tr>

    <tr>
      <td>Error</td>
      <td>-</td>
      <td>error</td>
      <td>There was an error completing the task.</td>
    </tr>
  </tbody>
</table>

The `dynamic` end state means that the exact values of this end state change based on how you define the block.


# UI Trigger
Source: https://dev.writer.com/blueprints/uitrigger



Triggers an event based on UI interactions like button clicks or input changes.

<img alt="" />

## Overview

The **UI Trigger** block is the starting point for your blueprint if users interact with the agent's interface.

It starts a workflow in response to a user action in the UI, such as clicking a button or typing in a text area. Use it to connect frontend events to backend logic in your blueprint.

You can specify the event to listen for and any parameters to pass to the workflow.

## How it works

1. **Component Id**: Select the UI component that triggers this workflow; for example, a button click or chatbot message.
2. **Event type**: Specify the type of event to listen for; for example, clicks (`wf-click`) or text area changes (`wf-change`).
3. **Default result**: Optionally provide a default result when testing the blueprint from the "Run blueprint" button. This is useful for testing the workflow without having to interact with the interface. The default result should match the expected payload format for the selected event type. For example:
   * For a button click (`wf-click`), provide an object with modifier key states, like:
     ```json theme={null}
     {
       "ctrl_key": false,
       "shift_key": false,
       "meta_key": false
     }
     ```
   * For a text input change (`wf-change`), provide the expected text value, like `"Sample text"`
   * For a file upload (`wf-file-change`), provide an array of file objects, like:
     ```json theme={null}
     [
       {
         "name": "example.txt",
         "type": "text/plain",
         "data": "data:text/plain;base64,U2FtcGxlIHRleHQ="
       }
     ]
     ```

When the specified event occurs on the selected component, the block starts the workflow. The event payload is available as `@{payload}` in subsequent blocks.

## Examples

### Interactive dashboard updates

This example demonstrates how to handle real-time dashboard interactions and updates.

**Interface:**

1. A [Plotly chart](/components/plotlygraph) that a user can click to trigger the workflow.

**Blueprint Flow:**

1. **UI Trigger** → User clicks on dashboard widget
2. **HTTP Request** → Fetches updated data from external API
3. **Set state** → Updates dashboard state

**Block Configuration:**

* **Component Id:** Dashboard widget component
* **Event type:** `wf-click`
* **Default result:** Optional, but useful for testing the blueprint from the "Run blueprint" button. The default result below mimics the data that would be returned if a user clicked somewhere on a [Plotly chart](/components/plotlygraph) in the interface.

```json theme={null}
[
  {
    "curveNumber": 0,
    "pointNumber": 1,
    "x": "Product B",
    "y": 150,
    "label": "Product B",
    "xaxis": {
      "anchor": "y"
    },
    "yaxis": {
      "anchor": "x"
    }
  }
]
```

This workflow enables real-time dashboard interactions with external data sources.

<img alt="" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Control</th>
    <th>Default</th>
    <th>Description</th>
    <th>Options</th>
    <th>Validation</th>
  </thead>

  <tbody>
    <tr>
      <td>Component Id</td>
      <td>Component Id</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The id of the component that will trigger this branch.</td>

      <td>
        uiComponentsWithEvents
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Event type</td>
      <td>Component Event Type</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The type of the event that will trigger this branch. For example, wf-click.</td>

      <td>
        eventTypes
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Default result</td>
      <td>Code</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>

      <td>The result that is used when the blueprint is triggered from the "Run blueprint" button</td>

      <td>
        <span>-</span>
      </td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## End states

Below are the possible end states of the block call.

<table>
  <thead>
    <th>Name</th>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
  </thead>

  <tbody>
    <tr>
      <td>Trigger</td>
      <td>-</td>
      <td>success</td>
      <td>-</td>
    </tr>
  </tbody>
</table>

The output of the **UI Trigger** block is the event payload. For example, if the event type is `wf-file-change` for a **File input** block, the payload contains the file that the user uploaded. You can access this value in any block in the blueprint as `@{payload}`.


# Annotated Text
Source: https://dev.writer.com/components/annotatedtext



Shows text with annotations

<img />

## Overview

The **Annotated Text** component displays text with visual annotations and highlights. It allows users to see text with embedded annotations that are color-coded to distinguish different types of content or sections.

Annotated text is essential for educational content, document review systems, and any interface where text needs visual distinction between different types of content. It provides a way to highlight important terms and sections without cluttering the main display.

## Common use cases

* **Educational content**: Provide explanations and definitions for complex terms
* **Document review**: Show comments, suggestions, and feedback on text
* **Code documentation**: Highlight and explain code snippets with annotations
* **Research papers**: Show citations, references, and additional context
* **Legal documents**: Display legal annotations and explanations

## How it works

1. **Text content**: Displays the main text content with embedded annotations
2. **Annotation markers**: Shows visual indicators for annotated sections
3. **Styling**: Customize appearance of annotations and text with colors and CSS classes

The component displays text with embedded annotations that are visually highlighted using color coding and styling to distinguish different types of content or sections.

## Configuration options

### Basic settings

* **Annotated text**: The main text with embedded annotation markers, must be a JSON string or a state reference to an array

### Advanced settings

* **Enable markdown**: Adds markdown support for automatically sanitizing unsafe elements
* **Enable copy buttons**: Adds a control bar with both copy text and JSON buttons
* **Rotate hue**: Rotates the hue of annotated text colors depending on the content of the string

### Styling options

* **Text color**: Set the color of the main text
* **Annotation colors**: Set colors for different annotation types
* **Custom CSS classes**: Apply additional styling

## Example

### Contract review and analysis

This example shows how to create a contract review interface with highlighted terms and clauses.

**Interface:**

* File input component for file upload
* Annotated text component for annotated contract content

**Annotated Text configuration:**

* **Annotated text**: JSON array containing contract text with embedded annotations in the format `["text", ["highlighted term", "label"], "more text"]`

## Best practices

1. **Clear annotations**: Make annotation content concise and informative
2. **Visual indicators**: Use clear visual cues to show where annotations exist
3. **Performance**: Limit the number of annotations to avoid overwhelming users
4. **Consistent styling**: Use consistent annotation styling throughout your interface
5. **Context relevance**: Ensure annotations provide relevant and helpful information

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Annotated text</td>
      <td>Object</td>
      <td>Value array with text/annotations. Must be a JSON string or a state reference to an array.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Reference</td>
      <td>Color</td>
      <td>The colour to be used as reference for chroma and luminance, and as the starting point for hue rotation.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Seed value</td>
      <td>Number</td>
      <td>Choose a different value to reshuffle colours.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Rotate hue</td>
      <td>Boolean</td>
      <td>If active, rotates the hue depending on the content of the string. If turned off, the reference colour is always used.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable markdown</td>
      <td>Boolean</td>
      <td>If active, the output will be sanitized; unsafe elements will be removed.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable copy button</td>
      <td>Boolean</td>
      <td>Enable a copy button that lets users to copy the contents in this field to their clipboard</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Button
Source: https://dev.writer.com/components/button



A standalone button component that can be linked to a click event handler.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Text</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Icon</td>
      <td>Text</td>
      <td>Lucide icon name in kebab-case, e.g. "badge-check".</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Disabled</td>
      <td>Boolean</td>
      <td>Disables all event handlers.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-click" icon="code">
    Capture single clicks.

    ```python theme={null}
    def handle_button_click(state):

    # Increment counter when the button is clicked

    state["counter"] += 1
    ```
  </Accordion>
</AccordionGroup>

<events />


# Chatbot
Source: https://dev.writer.com/components/chatbot



A chatbot component to build human-to-AI interactions.

<img />

Connect it to an LLM by handling the `wf-chatbot-message` event, which is triggered every time the user sends a message.

You can add `actions` to messages, which are buttons that trigger the `wf-chatbot-action-click`.

See the stubs for more details.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Conversation</td>
      <td>Object</td>
      <td>An array with messages or a variable that contains your conversation as an object.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Assistant initials</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>User initials</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable markdown</td>
      <td>Boolean</td>
      <td>If active, the output will be sanitized; unsafe elements will be removed.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable file upload</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <ol>
          <li>Single file</li>

          <li>Multiple files</li>

          <li>No</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Enable image paste</td>
      <td>Boolean</td>
      <td>Allow users to paste images directly into the chat input using Ctrl/Cmd+V.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Placeholder</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Assistant role</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>User role</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Avatar</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Avatar text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-chatbot-message" icon="code">
    Triggered when the user sends a message.

    ```python theme={null}
    def handle_message_simple(payload, state):

    # payload contains a dict in the form { "role": "user", "message": "hello"}

    state["conversation"] += [payload]
    state["conversation"] += [{
        "role": "assistant",
        "content": "Hello human" if payload == "Hello" else "I don't understand"
    }]

    # Handle streaming by appending to the last message

    import time
    for i in range(10):
        conv = state["conversation"]
        conv[-1]["content"] += f" {i}"
        state["conversation"] = conv
        time.sleep(0.5)
    ```
  </Accordion>

  <Accordion title="wf-chatbot-action-click" icon="code">
    Handle clicks on actions.

    ```python theme={null}
    def handle_action_simple(payload, state):

    # payload contains the "data" property of the action

    if payload == "change_title":
        state["app_background_color"] = "red"

    # Make an action available when adding a message

    def handle_message_with_action(payload, state):
    state["conversation"] += [payload]
    state["conversation"] += [{
        "role": "assistant",
        "content": "I don't know, but check this out.",
        "actions": [{
            "subheading": "Resource",
            "name": "Surprise",
            "desc": "Click to be surprised",
            "data": "change_title"
        }]
    }]
    ```
  </Accordion>

  <Accordion title="wf-file-change" icon="code">
    Triggered when files are uploaded

    ```python theme={null}
    def handle_file_upload(state, payload):

    # An array of dictionaries is provided in the payload
    # The dictionaries have the properties name, type and data
    # The data property is a file-like object

    uploaded_files = payload
    for i, uploaded_file in enumerate(uploaded_files):
        name = uploaded_file.get("name")
        file_data = uploaded_file.get("data")
        with open(f"{name}-{i}.jpeg", "wb") as file_handle:
            file_handle.write(file_data)
    ```
  </Accordion>
</AccordionGroup>

<events />


# Checkbox Input
Source: https://dev.writer.com/components/checkboxinput



A user input component that allows users to choose multiple values from a list of options using checkboxes.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Options</td>
      <td>Key-Value</td>
      <td>Key-value object with options. Must be a JSON string or a state reference to a dictionary.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Orientation</td>
      <td>Text</td>
      <td>Specify how to lay out the options.</td>

      <td>
        <ol>
          <li>Vertical</li>

          <li>Horizontal</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-options-change" icon="code">
    Sent when the selected options change.

    ```python theme={null}
    def onchange_handler(state, payload):

    # Set the state variable "selected" to the selected options.
    # The payload will be a list, as multiple options are allowed.

    state["selected"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Column
Source: https://dev.writer.com/components/column



A layout component that organizes its child components in columns. Must be inside a Column container component.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Title</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Width (factor)</td>
      <td>Number</td>
      <td>Relative size when compared to other columns in the same container. A column of width 2 will be double the width of one with width 1.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Sticky</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Collapsible</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Start collapsed</td>
      <td>Boolean</td>
      <td>Only applied when the column is collapsible.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Padding</td>
      <td>Padding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (H)</td>
      <td>Align (H)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (V)</td>
      <td>Align (V)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Column Container
Source: https://dev.writer.com/components/columns



Serves as container for Column components

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# DataFrame
Source: https://dev.writer.com/components/dataframe



A component to display Pandas DataFrames.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Data</td>
      <td>Text</td>
      <td>Must be a state reference to a Pandas dataframe or PyArrow table. Alternatively, a URL for an Arrow IPC file.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Show index</td>
      <td>Boolean</td>
      <td>Shows the dataframe's index. If an Arrow table is used, shows the zero-based integer index.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable search</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable adding a record</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable updating a record</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable download</td>
      <td>Boolean</td>
      <td>Allows the user to download the data as CSV.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Actions</td>
      <td>Key-Value</td>
      <td>Define rows actions</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable markdown</td>
      <td>Boolean</td>
      <td>If active, the output will be sanitized; unsafe elements will be removed.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Display row count</td>
      <td>Number</td>
      <td>Specifies how many rows to show simultaneously.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Wrap text</td>
      <td>Boolean</td>
      <td>Not wrapping text allows for an uniform grid, but may be inconvenient if your data contains longer text fields.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Font style</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <ol>
          <li>normal</li>

          <li>monospace</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-dataframe-add" icon="code">
    Capture adding a row.

    ```python theme={null}
    # Subscribe this event handler to the `wf-dataframe-add` event
    #
    # more on : https://dev.writer.com/framework/dataframe
    def on_record_add(state, payload):
    payload['record']['sales'] = 0 # default value inside the dataframe
    state['mydf'].record_add(payload) # should contain an EditableDataFrame instance
    ```
  </Accordion>

  <Accordion title="wf-dataframe-update" icon="code">
    Capture a cell change.

    ```python theme={null}
    # Subscribe this event handler to the `wf-dataframe-update` event
    #
    # more on : https://dev.writer.com/framework/dataframe
    def on_record_change(state, payload):
    state['mydf'].record_update(payload) # should contain an EditableDataFrame instance
    ```
  </Accordion>

  <Accordion title="wf-dataframe-action" icon="code">
    Remove or open a row.

    ```python theme={null}
    # Subscribe this event handler to the `wf-dataframe-action` event
    #
    # more on : https://dev.writer.com/framework/dataframe
    def on_record_action(state, payload):
    # state['mydf'] should contains an EditableDataFrame instance
    record_index = payload['record_index']
    if payload['action'] == 'remove':
    	state['mydf'].record_remove(payload) # should contain an EditableDataFrame instance
    if payload['action'] == 'open':
    	state['record'] = state['mydf'].record(record_index) # dict representation of record
    ```
  </Accordion>
</AccordionGroup>

<events />


# Date Input
Source: https://dev.writer.com/components/dateinput



A user input component that allows users to select a date using a date picker interface.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-date-change" icon="code">
    Capture changes to this control.

    ```python theme={null}

    def onchange_handler(state, payload):

    # Set the state variable "new_date" to the new value, provided as a YYYY-MM-DD string.

    state["new_date"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# File Input
Source: https://dev.writer.com/components/fileinput



A user input component that allows users to upload files.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Allowed file types</td>
      <td>Text</td>
      <td>Provides hints for browsers to select the correct file types. You can specify extensions and MIME types separated by comma, or leave empty to accept any file.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Allow multiple files</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-file-change" icon="code">
    Capture changes to this control.

    ```python theme={null}
    def onchange_handler(state, payload):

    # An array of dictionaries is provided in the payload
    # The dictionaries have the properties name, type and data
    # The data property is a file-like object

    uploaded_files = payload
    for i, uploaded_file in enumerate(uploaded_files):
    	name = uploaded_file.get("name")
        file_data = uploaded_file.get("data")
        with open(f"{name}-{i}.jpeg", "wb") as file_handle:
            file_handle.write(file_data)
    ```
  </Accordion>
</AccordionGroup>

<events />


# Heading
Source: https://dev.writer.com/components/heading



A text component used to display headings or titles in different sizes and styles.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Text</td>
      <td>Text</td>
      <td>Add text directly, or reference state elements with @.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Heading type</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <ol>
          <li>h1 (Big)</li>

          <li>h2 (Normal)</li>

          <li>h3 (Small)</li>

          <li>h4 (Smallest)</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Alignment</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <ol>
          <li>Left</li>

          <li>Center</li>

          <li>Right</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Horizontal Stack
Source: https://dev.writer.com/components/horizontalstack



A layout component that stacks its child components horizontally, wrapping them to the next row if necessary.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Padding</td>
      <td>Padding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (H)</td>
      <td>Align (H)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (V)</td>
      <td>Align (V)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# HTML Element
Source: https://dev.writer.com/components/html



A generic component that creates customisable HTML elements, which can serve as containers for other components.

<img />

You can configure the element type, styles, and attributes to fit your design requirements. You can link them to state for advanced use cases, such as custom animations.

All valid HTML tags are supported, including tags such as `iframe`, allowing you to embed external sites.

Take into account the potential risks of adding custom HTML to your app, including XSS. Be specially careful when injecting user-generated data.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Element</td>
      <td>Text</td>
      <td>Set the type of HTML element to create, e.g., 'div', 'section', 'span', etc.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Styles</td>
      <td>Object</td>
      <td>Define the CSS styles to apply to the HTML element using a JSON object or a state reference to a dictionary.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Attributes</td>
      <td>Object</td>
      <td>Set additional HTML attributes for the element using a JSON object or a dictionary via a state reference.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>HTML inside</td>
      <td>Text</td>
      <td>Define custom HTML to be used inside the element. It will be wrapped in a div and rendered after children components.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# IFrame
Source: https://dev.writer.com/components/iframe



A component to embed an external resource in an iframe.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Source</td>
      <td>Text</td>
      <td>A valid URL</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Referrer Policy</td>
      <td>Text</td>
      <td>Define which referrer is sent when fetching the resource</td>

      <td>
        <ol>
          <li>no-referrer</li>

          <li>no-referrer-when-downgrade</li>

          <li>origin</li>

          <li>origin-when-cross-origin</li>

          <li>same-origin</li>

          <li>strict-origin</li>

          <li>strict-origin-when-cross-origin</li>

          <li>unsafe-url</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-load" icon="code">
    Fires when the resource has successfully loaded.

    ```python theme={null}
    def load_handler(state):

    # Sets status message when resource is loaded

    state["status"] = "Page loaded"
    ```
  </Accordion>
</AccordionGroup>

<events />


# Image
Source: https://dev.writer.com/components/image



A component to display images.

<img />

Use your app's static folder to serve images directly. For example, `static/my_image.png`.

Alternatively, pass a Matplotlib figure via state.

`state[&quot;my_fig&quot;] = fig` and then setting the *Image* source to `@{fig}`

You can also use packed files or bytes:

`state[&quot;img_b&quot;] = wf.pack_bytes(img_bytes, &quot;image/png&quot;)`

`state[&quot;img_f&quot;] = wf.pack_file(img_file, &quot;image/png&quot;)`

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Source</td>
      <td>Text</td>
      <td>A valid URL. Alternatively, you can provide a state reference to a Matplotlib figure or a packed file.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Caption</td>
      <td>Text</td>
      <td>Leave blank to hide.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Max width (px)</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Max height (px)</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-click" icon="code">
    Capture single clicks.

    ```python theme={null}
    def click_handler(state):

    # Increment counter when the image is clicked

    state["counter"] += 1
    ```
  </Accordion>
</AccordionGroup>

<events />


# JSON Viewer
Source: https://dev.writer.com/components/jsonviewer



A component to explore JSON data as a hierarchy.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Data</td>
      <td>Object</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Initial depth</td>
      <td>Number</td>
      <td>Sets the initial viewing depth of the JSON tree hierarchy. Use -1 to display the full hierarchy.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Hide root</td>
      <td>Boolean</td>
      <td>Don't show the type of the root node when it's an Object or an Array.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable copy button</td>
      <td>Boolean</td>
      <td>Enable a copy button that lets users to copy the contents in this field to their clipboard</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>JSON indentation</td>
      <td>Width</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Link
Source: https://dev.writer.com/components/link



A component to create a hyperlink.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>URL</td>
      <td>Text</td>
      <td>Specify a URL or choose a page. Keep in mind that you can only link to pages for which a key has been specified.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Target</td>
      <td>Text</td>
      <td>Specifies where to open the linked document.</td>

      <td>
        <ol>
          <li>Self</li>

          <li>Blank</li>

          <li>Parent</li>

          <li>Top</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Rel</td>
      <td>Text</td>
      <td>Specifies the relationship between the current document and the linked document.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Text</td>
      <td>Text</td>
      <td>The text to display in the link.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Message
Source: https://dev.writer.com/components/message



A component that displays a message in various styles, including success, error, warning, and informational.

<img />

## Overview

The **Message** component displays status messages, notifications, and feedback to users. It uses prefix characters to determine the message type and styling automatically.

Message components support different types (success, error, warning, loading, info) with appropriate styling and can display dynamic content from state variables. They're essential for creating responsive, user-friendly interfaces that keep users informed.

## Common use cases

* **Status updates**: Show progress messages during processing operations
* **Error notifications**: Display validation errors or system failures
* **Success confirmations**: Confirm successful actions or completions
* **Loading states**: Show animated loading messages during operations
* **Form validation**: Display field validation errors and warnings

## How it works

1. **Message content**: Display static text or reference state variables
2. **Prefix-based types**: Use prefix characters to determine message type automatically. For example, `+` makes the message green, `-` makes it red, and `%` provides a dynamic loading icon.
3. **Dynamic updates**: Content updates automatically when state variables change
4. **Visual styling**: Each message type has distinct colors and styling
5. **Conditional display**: Messages can show or hide based on conditions

The component provides immediate feedback to users about the state of their interactions and system operations.

## Configuration options

### Basic settings

* **Message**: The text content to display (supports state variable references)
* **Prefix characters**: Use prefixes to determine message type automatically. For example, `+` in front of the message makes it green: `+Operation completed successfully`.

### Message types and prefixes

* **Success**: Prefix with `+` (for example, `"+Operation completed successfully"`)
* **Error**: Prefix with `-` (for example, `"-An error occurred"`)
* **Warning**: Prefix with `!` (for example, `"!Please check your input"`)
* **Loading**: Prefix with `%` (for example, `"%Processing your request..."`)
* **Info**: No prefix (for example, `"This is an informational message"`)

### Styling options

* **Success color**: Customize success message background color (default: Green3)
* **Error color**: Customize error message background color (default: Orange2)
* **Warning color**: Customize warning message background color (default: #FFE999)
* **Info color**: Customize info message background color (default: Blue2)
* **Loading color**: Customize loading message background color (default: Blue2)
* **Primary text color**: Set the text color
* **Custom CSS classes**: Apply additional styling

## Example

### Processing status updates

Create a workflow that shows different status messages during file processing.

**Interface:**

* File input to upload a file
* Button component to start file processing
* Message component for status updates

**Message configuration:**

* **Message**: `@{status_message}` (with `%` prefix for loading, `+` for success, `-` for errors)

<img alt="Message configuration" />

## Best practices

1. **Clear messaging**: Use concise, clear language that users can understand immediately
2. **Appropriate types**: Choose the correct message type for the content (info, success, warning, error)
3. **Dynamic content**: Use state variables to show relevant, up-to-date information
4. **Conditional display**: Show messages only when relevant to avoid interface clutter
5. **Consistent styling**: Maintain consistent message styling across your interface
6. **Context**: Provide enough context for users to understand and act on the message

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Message</td>
      <td>Text</td>
      <td>Prefix with '+' for a success message, with '-' for error, '!' for warning, '%' for loading. No prefix for info. Leave empty to hide.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Success</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Error</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Warning</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Info</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Loading</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Metric
Source: https://dev.writer.com/components/metric



A component that prominently displays a metric value and associated information.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Name</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value</td>
      <td>Text</td>
      <td>The main value to be displayed. It's not limited to numbers.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Description</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Note</td>
      <td>Text</td>
      <td>Prefix with '+' for a positive message, with '-' for a negative message.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Positive</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Neutral</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Negative</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Multiselect Input
Source: https://dev.writer.com/components/multiselectinput



A user input component that allows users to select multiple values from a searchable list of options.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Options</td>
      <td>Key-Value</td>
      <td>Key-value object with options. Must be a JSON string or a state reference to a dictionary.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Placeholder</td>
      <td>Text</td>
      <td>Text to show when no options are selected.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Maximum count</td>
      <td>Number</td>
      <td>The maximum allowable number of selected options. Set to zero for unlimited.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>The colour of the chips created for each selected option.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Chip text</td>
      <td>Color</td>
      <td>The colour of the text in the chips.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-options-change" icon="code">
    Sent when the selected options change.

    ```python theme={null}
    def onchange_handler(state, payload):

    # Set the state variable "selected" to the selected option

    state["selected"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Number Input
Source: https://dev.writer.com/components/numberinput



A user input component that allows users to enter numeric values.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Placeholder</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Minimum value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Max value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Step</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-number-change" icon="code">
    Capture changes as they happen.

    ```python theme={null}

    def onchange_handler(state, payload):

    # Set the state variable "new_val" to the new value

    state["new_val"] = payload
    ```
  </Accordion>

  <Accordion title="wf-number-change-finish" icon="code">
    Capture changes once this control has lost focus.

    ```python theme={null}

    def onchange_handler(state, payload):

    # Set the state variable "new_val" to the new value

    state["new_val"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Page
Source: https://dev.writer.com/components/page



A container component representing a single page within the application.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Page key</td>
      <td>Identifying Key</td>
      <td>Unique identifier. It's needed to enable navigation to this Page.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Page mode</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <ol>
          <li>Compact</li>

          <li>Wide</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Emptiness</td>
      <td>Color</td>
      <td>Page background color</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-keydown" icon="code">
    Captures all key activity while this page is open.

    ```python theme={null}

    def handle_keydown(state, payload):
    # The payload is a dictionary containing the key code and modifier keys.
    # For example,
    # {
    #	"key": "ArrowDown",
    #	"ctrl_key": False,
    #	"shift_key": False,
    #	"meta_key": False
    # }

    key_activated = payload.get("key")
    delta = 0
    if key_activated == "ArrowLeft":
    	delta += -10
    elif key_activated == "ArrowRight":
    	delta += 10

    shift_key = payload.get("shift_key")
    if shift_key:
    	delta *= 2 # Shift makes it go faster

    state["position"] += delta

    ```
  </Accordion>

  <Accordion title="wf-page-open" icon="code">
    Emitted when the page is opened.

    ```python theme={null}

    def handle_page_open(state, payload):
    page_key = payload
    state["message"] = f"The page {page_key} has been opened."

    ```
  </Accordion>
</AccordionGroup>

<events />


# Pagination
Source: https://dev.writer.com/components/pagination



A component that can help you paginate records, for example from a Repeater or a DataFrame.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Page</td>
      <td>Number</td>
      <td>The current page number.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Page Size</td>
      <td>Number</td>
      <td>The number of items per page.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Total Items</td>
      <td>Number</td>
      <td>The total number of items</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Page Size Options</td>
      <td>Text</td>
      <td>A comma-separated list of page size options. If it's empty, the user can't change the page size. Set your default page size as the first option.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Show All Option</td>
      <td>Boolean</td>
      <td>Show an option to show all records.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Jump To</td>
      <td>Boolean</td>
      <td>Show an option to jump to a specific page.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-change-page" icon="code">
    Fires when the user pick a page

    ```python theme={null}
    def handle_page_change(state, payload):
    page = payload
    state["page"] = page

    records = _load_records_from_db(start = state["page"] * state["pageSize"], limit = state["pageSize"])
    # update a repeater
    state["highlighted_members"] = {r.id: r for r in records}
    ```
  </Accordion>

  <Accordion title="wf-change-page-size" icon="code">
    Fires when the user change the page size.

    ```python theme={null}
    def handle_page_size_change(state, payload):
    state['pageSize'] = payload

    records = _load_records_from_db(start = state["page"] * state["pageSize"], limit = state["pageSize"])
    # update a repeater
    state["highlighted_members"] = {r.id: r for r in records}
    ```
  </Accordion>
</AccordionGroup>

<events />


# PDF
Source: https://dev.writer.com/components/pdf



A component to embed PDF documents.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>PDF source</td>
      <td>Text</td>
      <td>A valid URL. Alternatively, you can provide a state reference to a packed PDF file.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Highlights</td>
      <td>Object</td>
      <td>A list of highlights to be applied to the PDF as a JSON array of strings.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Selected highlight match</td>
      <td>Number</td>
      <td>The index of the selected highlight match.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Page</td>
      <td>Number</td>
      <td>The page to be displayed.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable controls</td>
      <td>Boolean</td>
      <td>Show controls to navigate the PDF.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Plotly Graph
Source: https://dev.writer.com/components/plotlygraph



A component that displays Plotly graphs.

<img />

You can listen to events triggered by Plotly.js and add interactivity to your charts.
For example, implement cross-filtering.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Graph specification</td>
      <td>Object</td>
      <td>Plotly graph specification. Pass it using state, e.g. @, or paste a JSON specification.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="plotly-click" icon="code">
    Sends a list with the clicked points.

    ```python theme={null}
    ```
  </Accordion>

  <Accordion title="plotly-selected" icon="code">
    Sends a list with the selected points.

    ```python theme={null}
    ```
  </Accordion>

  <Accordion title="plotly-deselect" icon="code">
    Triggered when points are deselected.

    ```python theme={null}
    ```
  </Accordion>
</AccordionGroup>

<events />


# Progress Bar
Source: https://dev.writer.com/components/progressbar



A component to display a progression.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Minimum value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Maximum value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Display percentage</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-click" icon="code">
    Triggered when the Progress bar is clicked.

    ```python theme={null}
    def handle_progress_bar_click():
    print("The Progress bar was clicked")
    ```
  </Accordion>
</AccordionGroup>

<events />


# Radio Input
Source: https://dev.writer.com/components/radioinput



A user input component that allows users to choose a single value from a list of options using radio buttons.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Options</td>
      <td>Key-Value</td>
      <td>Key-value object with options. Must be a JSON string or a state reference to a dictionary.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Orientation</td>
      <td>Text</td>
      <td>Specify how to lay out the options.</td>

      <td>
        <ol>
          <li>Vertical</li>

          <li>Horizontal</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-option-change" icon="code">
    Sent when the selected option changes.

    ```python theme={null}
    def onchange_handler(state, payload):

    # Set the state variable "selected" to the selected radio option

    state["selected"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Slider Range Input
Source: https://dev.writer.com/components/rangeinput



A user input component that allows users to select numeric values range using a range slider with optional constraints like min, max, and step.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Minimum value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Maximum value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Step size</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Popover color</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Popover background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-range-change" icon="code">
    Capture changes to this control.

    ```python theme={null}

    def onchange_handler(state, payload):

    # Set the state variables "from" & "to" to the new range
    state["from"] = payload[0]
    state["to"] = payload[1]
    ```
  </Accordion>
</AccordionGroup>

<events />


# Rating Input
Source: https://dev.writer.com/components/ratinginput



A user input component that allows users to provide a rating.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Feedback</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <ol>
          <li>Stars</li>

          <li>Faces</li>

          <li>Hearts</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Minimum value</td>
      <td>Number</td>
      <td>Valid values are 0 and 1.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Max value</td>
      <td>Number</td>
      <td>Valid values are between 2 and 11.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Step</td>
      <td>Number</td>
      <td>Valid values are between 0.25 and 1.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-number-change" icon="code">
    ```python theme={null}

    def onchange_handler(state, payload):

    # Set the state variable "rating" to the new value

    state["rating"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Repeater
Source: https://dev.writer.com/components/repeater



A container component that repeats its child components based on a dictionary.

<img />

## Overview

The **Repeater** component dynamically creates copies of its child components based on a dictionary or array. It's similar to a for-each loop in programming, but for UI elements. Use Repeaters when you need to display lists, collections, or any data where you don't know the exact number of items in advance.

Inside a Repeater, you can access:

* `@{item}`: the current item's value
* `@{itemId}`: the current item's index (for arrays) or key (for dictionaries)

## Common use cases

* **Dynamic lists**: Display files, tasks, messages, or any collection of items
* **Search results**: Show filtered or API-fetched data
* **User-generated content**: Display comments, reviews, or form responses
* **Product catalogs**: Render product cards or inventory items
* **Data tables**: Create rows based on database queries or structured output

## How it works

1. **Data source**: Bind the Repeater to a state variable containing an array or dictionary
2. **Child components**: Add components inside the Repeater that define the template
3. **Variable access**: Use `@{item}` and `@{itemId}` to display data for each iteration
4. **Nested Repeaters**: Nest Repeaters to display hierarchical data structures
5. **Input binding**: Bind input components to specific items using `@{itemId}` for interactive lists

## Configuration options

### Basic settings

* **Repeater object**: The state variable containing the array or dictionary to iterate over (for example, `@{myList}`)
* **Key variable name**: The variable name for accessing the current item's key or index (default: `itemId`)
* **Value variable name**: The variable name for accessing the current item's value (default: `item`)

### Visibility

Set custom visibility conditions to show the Repeater only when data is available (for example, `myList` to hide when empty).

## Using inputs within Repeaters

When you add input components (like Text Input, Text Area Input, or other form fields) inside a Repeater, you need to properly bind them to capture user input for each repeated item.

### Binding inputs to dictionary items

The most reliable approach is to use a dictionary as your Repeater data source and bind inputs using the `itemId` to target specific dictionary keys:

```
state_variable[itemId].property_name
```

**Example - Text Area Input inside a Repeater**

If your state variable is `products` and you want each product to have a `description` property that users can edit:

1. Set up your Repeater with **Repeater object**: `@{products}`
2. Add a Text Area Input inside the Repeater
3. Set the Text Area Input's **Link variable** to: `products[itemId].description`

This creates or updates the `description` key for each specific product in your dictionary. As users type in each Text Area Input, the corresponding product's description updates in your state.

### Managing editable lists with temporary state

For more complex scenarios where you need to edit one item at a time:

1. **Create a temporary state variable** to hold the draft being edited (for example, `current_draft`)
2. **Track which item is being edited** using a state variable like `editing_item_id`
3. **Show input fields conditionally** using visibility rules that check if `@{itemId} == @{editing_item_id}`
4. **Display read-only content** for items that aren't being edited
5. **Copy data** from the temporary variable back to your main data structure when the user saves

This pattern prevents multiple items from being edited simultaneously and provides a clearer user experience when editing items in a list.

### Best practices for Repeater inputs

1. **Use dictionaries over arrays**: Dictionaries make it easier to target specific items using `itemId` as the key
2. **Provide clear feedback**: Show which item is being edited with conditional styling or visibility
3. **Validate input**: Check that required fields are filled before processing
4. **Consider performance**: Large lists with many input components can impact performance; use pagination if needed
5. **Test empty states**: Ensure your Repeater handles empty data gracefully

## Example: Dynamic task list with input

This example shows how to create an editable task list where users can update task descriptions.

**Interface:**

* Button to add new tasks
* Repeater bound to `tasks` dictionary
* Text Input inside Repeater for editing task names
* Button inside Repeater to delete tasks

**Repeater configuration:**

* **Repeater object**: `@{tasks}`
* **Key variable name**: `itemId`
* **Value variable name**: `item`

**Text Input configuration (inside Repeater):**

* **Link variable**: `tasks[itemId].name`
* **Placeholder**: "Enter task name"

This setup allows each task to maintain its own editable name field.

## Dynamic tabs

Repeaters can be used to create tabs dynamically based on your data. Place a **Repeater** inside a **Tab Container**, then add a **Tab** component inside the Repeater.

**Example structure:**

* Tab Container
  * Repeater (bound to `@{categories}`)
    * Tab (with name `@{item.name}`)
      * Your content here

Each item in your data generates a new tab. This is useful for category-based views, multi-entity interfaces, or any scenario where the number of tabs depends on your data.

## Dynamic columns

Similarly, you can create dynamic column layouts by placing a **Repeater** inside a **Column Container** with **Column** components inside the Repeater. This allows for responsive, data-driven layouts.

## Working with pagination

For large datasets, combine Repeaters with the **Pagination** component to improve performance. The Pagination component provides events (`wf-change-page`, `wf-change-page-size`) that you can handle in your blueprints to update the data displayed in your Repeater.

**Example pattern:**

1. Add a Pagination component to your interface
2. Handle the `wf-change-page` event to load the appropriate data slice
3. Update the state variable bound to your Repeater with the paginated data

This approach prevents rendering hundreds or thousands of items at once, which can impact performance.

## Additional resources

For a detailed tutorial on building with Repeaters, including nested Repeaters and structured output integration, see [Build dynamic UI components with Repeaters](/agent-builder/repeater).

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Repeater object</td>
      <td>Object</td>
      <td>Include a state reference to the dictionary used for repeating the child components. Alternatively, specify a JSON object.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Key variable name</td>
      <td>Text</td>
      <td>Set the name of the variable that will store the key of the current repeater object entry.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Value variable name</td>
      <td>Text</td>
      <td>Set the name of the variable that will store the value of the current repeater object entry.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Reuse Component
Source: https://dev.writer.com/components/reuse



Those components are used to reuse other components. Reused components share the same state and are updated together.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Component id</td>
      <td>Text</td>
      <td>The id of the component to reuse.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Root
Source: https://dev.writer.com/components/root



The root component of the application, which serves as the starting point of the component hierarchy.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>App name</td>
      <td>Text</td>
      <td>The app name will be shown in the browser's title bar.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Padding</td>
      <td>Padding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Emptiness</td>
      <td>Color</td>
      <td>Page background color</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (V)</td>
      <td>Align (V)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (H)</td>
      <td>Align (H)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content width</td>
      <td>Width</td>
      <td>Configure content width using CSS units, e.g. 100px, 50%, 10vw, etc.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-app-open" icon="code">
    Captures the first application load, including page key and route vars.

    ```python theme={null}
    def handle_app_open(state):
    # The payload is a dictionary with the page key and all the route variables in the URL hash.
    # For example, if the current URL is http://localhost:3000/#/animal=duck&colour=yellow
    # you will get the following dictionary
    # {
    #   "page_key": "main",
    #	  "route_vars": {
    #		  "animal": "duck",
    #		  "colour": "yellow"
    #	  }
    # }

    page_key = payload.get("page_key")
    route_vars = payload.get("route_vars")
    ```
  </Accordion>

  <Accordion title="wf-hashchange" icon="code">
    Capture changes to the URL hash, including page key and route vars.

    ```python theme={null}
    def handle_hashchange(state, payload):
    # The payload is a dictionary with the page key and all the route variables in the URL hash.
    # For example, if the current URL is http://localhost:3000/#main/animal=duck&colour=yellow
    # you will get the following dictionary
    # {
    #	  "page_key": "main",
    #	  "route_vars": {
    #		  "animal": "duck",
    #		  "colour": "yellow"
    #	  }
    # }

    page_key = payload.get("page_key")
    route_vars = payload.get("route_vars")

    if not route_vars:
    	return

    if route_vars.get("animal") == "duck":
    	state["message"] = "You've navigated to the Duck zone."
    else:
    	state["message"] = "You're not in the Duck zone.
    ```
  </Accordion>
</AccordionGroup>

<events />


# Section
Source: https://dev.writer.com/components/section



A container component that divides the layout into sections, with an optional title.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Title</td>
      <td>Text</td>
      <td>Leave blank to hide.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Collapsible</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Start collapsed</td>
      <td>Boolean</td>
      <td>Only applied when the component is collapsible.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Padding</td>
      <td>Padding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (H)</td>
      <td>Align (H)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Select Input
Source: https://dev.writer.com/components/selectinput



A user input component that allows users to select a single or multiples value(s) from a searchable list of options.

<img />

## Overview

The **Select Input** component provides a dropdown interface for selecting single or multiple values from a searchable list of options. It offers a clean, accessible way to present multiple choices while maintaining a compact interface.

Select inputs are essential for forms and interfaces where users need to choose from predefined options. They provide better user experience than text inputs for categorical data and help ensure data consistency and validation.

## Common use cases

* **Form selections**: Choose options in registration and configuration forms
* **Category filtering**: Filter content by categories or types with single or multiple selections
* **Configuration settings**: Select preferences and settings from predefined options
* **Data entry**: Choose from standardized options for consistent data entry

## How it works

1. **Label**: Provide a clear label for the select input field
2. **Options**: Define available choices as key-value pairs in JSON format
3. **Dropdown**: Display options in a searchable dropdown interface
4. **Selection**: Allow users to select one or multiple options from the list
5. **Binding**: Connect to a state variable to store the selected value
6. **Events**: Trigger workflows when selections change

The component automatically updates the bound state variable when users make selections, and can trigger events for real-time processing or validation.

## Configuration options

### Basic settings

* **Label**: Text label displayed preceding the select input field
* **Allow Multi-select**: Toggle to enable selection of multiple options from the dropdown (default: off)
* **Options**: Key-value object with available options (must be a JSON string or state reference to a dictionary)
* **Placeholder**: Text to display when no options are selected
* **Maximum count**: The maximum allowable number of selected options (set to zero for unlimited) - only available when multi-select is enabled

### Binding settings

* **Link Variable**: Connect the result of this component to a dynamic variable for use across the agent

### Styling options

* **Accent color**: Set the focus and highlight color for the select input
* **Primary text color**: Set the main text color for options and labels
* **Container background color**: Set the background color of the dropdown container
* **Separator color**: Set the border and divider color
* **Custom CSS classes**: Apply additional styling with custom CSS classes (separated by spaces)

### Events

* **wf-option-change**: Triggered when a single option changes (only available when multi-select is off) - payload contains the selected option
* **wf-options-change**: Triggered when multiple options change (only available when multi-select is enabled) - payload contains array of selected options

## Example

### User registration form

This example shows how to create a registration form with select inputs.

**Interface:**

* Text Input components for personal information
* Select Input components for preferences
* Button component for submission

**Select Input configuration:**

* **Label**: "Country," "Account Type," "Time Zone"
* **Options**: Various country, account type, and timezone options
* **Link Variable**: "country," "account," "timezone"

### Multi-select tag management

Create a tag management interface for content categorization.

**Interface:**

* Select Input component with multi-select enabled
* Button component for saving

**Select Input configuration:**

* **Label**: "Content Tags"
* **Allow Multi-select**: Enabled
* **Options**: JSON object with tag categories
* **Placeholder**: "Select tags for your content"
* **Link Variable**: "tags"

## Best practices

1. **Clear labeling**: Use descriptive labels that explain the selection purpose and context
2. **Logical ordering**: Arrange options in logical order (alphabetical, numerical, or by frequency of use)
3. **Option clarity**: Use clear, concise option text that users understand without ambiguity
4. **Accessibility**: Ensure select inputs are keyboard navigable and screen reader friendly

This component provides a searchable dropdown for selecting one or multiple values. Usage: supply the Options field with a key-value object; open the dropdown and use the search box at the top to filter options by label (and detail, when provided). Configuration: Enable multi-selection via the 'Allow Multi-select' field; set a placeholder and limit selections with 'Maximum count' (applies only to multi-select). Limitations: search is client-side, case-insensitive, and matches label/detail text only (not the option key); no remote/async search; very large option lists are filtered in-memory.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Allow Multi-select</td>
      <td>Boolean</td>
      <td>Select more than one option from the dropdown.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Options</td>
      <td>Key-Value</td>
      <td>Key-value object with options. Must be a JSON string or a state reference to a dictionary.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Placeholder</td>
      <td>Text</td>
      <td>Text to show when no options are selected.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Maximum count</td>
      <td>Number</td>
      <td>The maximum allowable number of selected options. Set to zero for unlimited.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Chip text</td>
      <td>Color</td>
      <td>The colour of the text in the chips.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-option-change" icon="code">
    Sent when the selected option changes.

    ```python theme={null}
    def onchange_handler(state, payload):

    # Set the state variable "selected" to the selected option

    state["selected"] = payload
    ```
  </Accordion>

  <Accordion title="wf-options-change" icon="code">
    Sent when the selected options change.

    ```python theme={null}
    def onchange_handler(state, payload):

    # Set the state variable "selected" to the selected option

    state["selected"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Separator
Source: https://dev.writer.com/components/separator



A visual component to create a separation between adjacent elements.

<img />

*Separator* components are used to separate layout elements. They can be used in most containers, including *Column container* to separate columns.

If the container flows horizontally (like a *Horizontal Stack* or a *Column container*) the *Separator* will be a vertical line. Otherwise, it'll be a horizontal line.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Sidebar
Source: https://dev.writer.com/components/sidebar



A container component that organizes its children in a sidebar. Its parent must be a Page component.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Start collapsed</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Slider Input
Source: https://dev.writer.com/components/sliderinput



A user input component that allows users to select numeric values using a slider with optional constraints like min, max, and step.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Minimum value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Maximum value</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Step size</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Popover color</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Popover background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-number-change" icon="code">
    Capture changes to this control.

    ```python theme={null}

    def onchange_handler(state, payload):

    # Set the state variable "new_val" to the new value

    state["new_val"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Step
Source: https://dev.writer.com/components/step



A container component that displays its child components as a step inside a Step container.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Name</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Padding</td>
      <td>Padding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Completed</td>
      <td>Boolean</td>
      <td>Use a state reference to dynamically mark this step as complete.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (H)</td>
      <td>Align (H)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Step Container
Source: https://dev.writer.com/components/steps



A container component for displaying Step components, allowing you to implement a stepped blueprint.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Switch Input
Source: https://dev.writer.com/components/switchinput



A user input component with a simple on/off status.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-toggle" icon="code">
    Sent when the switch is toggled.

    ```python theme={null}
    def handle_toggle(state, payload):

    # The payload will be a bool 

    state["its_on"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Tab
Source: https://dev.writer.com/components/tab



A container component that displays its child components as a tab inside a Tab container.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Name</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Padding</td>
      <td>Padding</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Content alignment (H)</td>
      <td>Align (H)</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Tab Container
Source: https://dev.writer.com/components/tabs



A container component for organising and displaying Tab components in a tabbed interface.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Secondary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container background</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Container shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Separator</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Button shadow</td>
      <td>Shadow</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-tab-change" icon="code">
    Sent when the active tab changes.

    ```python theme={null}
    def tab_change_handler(state, payload):

    # The payload contains the name of the newly activated tab

    state["active_tab"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Tags
Source: https://dev.writer.com/components/tags



A component to display coloured tag pills.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Tags</td>
      <td>Key-Value</td>
      <td>Key-value object with tags. Must be a JSON string or a state reference to a dictionary.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Reference</td>
      <td>Color</td>
      <td>The colour to be used as reference for chroma and luminance, and as the starting point for hue rotation.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Seed value</td>
      <td>Number</td>
      <td>Choose a different value to reshuffle colours.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Rotate hue</td>
      <td>Boolean</td>
      <td>If active, rotates the hue depending on the content of the string. If turned off, the reference colour is always used.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-tag-click" icon="code">
    Triggered when a tag is clicked.

    ```python theme={null}
    def handle_tag_click(state, payload):
    state["selected_tag_id"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Text
Source: https://dev.writer.com/components/text



A component to display plain text or formatted text using Markdown syntax.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Text</td>
      <td>Text</td>
      <td>Add text directly, or reference state elements with @.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable markdown</td>
      <td>Boolean</td>
      <td>The Markdown output will be sanitised; unsafe elements will be removed.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Alignment</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <ol>
          <li>Left</li>

          <li>Center</li>

          <li>Right</li>
        </ol>
      </td>
    </tr>

    <tr>
      <td>Enable copy button</td>
      <td>Boolean</td>
      <td>Enable a copy button that lets users to copy the contents in this field to their clipboard</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Primary text</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-click" icon="code">
    Capture single clicks.

    ```python theme={null}
    def click_handler(state):

    # Increment counter when the text is clicked

    state["counter"] += 1
    ```
  </Accordion>
</AccordionGroup>

<events />


# Text area Input
Source: https://dev.writer.com/components/textareainput



A user input component that allows users to enter multi-line text values.

<img />

## Overview

The **Text area Input** component allows users to enter multi-line text values. It's designed for longer text content like descriptions, comments, reviews, or any content that requires multiple lines.

## Common use cases

* **Long-form content**: Collect essays, articles, or detailed descriptions
* **User feedback**: Gather reviews, comments, or support requests
* **Data entry**: Allow users to paste large amounts of text or data
* **Configuration**: Collect complex configuration settings or parameters

## How it works

1. **Multi-line input**: Users can enter text across multiple lines
2. **Manual resizing**: Users can resize the text area by dragging the bottom corner
3. **State binding**: Content automatically saves to the specified state variable
4. **Real-time events**: Triggers events as users type or finish editing
5. **Configurable rows**: Set the initial number of rows to display

The component provides a familiar text editing experience with manual resizing for longer content entry.

## Configuration options

### Basic settings

* **Label**: Text label displayed above the input field
* **Placeholder**: Hint text shown when the field is empty
* **Rows**: Number of rows to display (default: `5`)
* **Link variable**: The name of the state variable that stores the input value

### Styling options

* **Custom CSS classes**: Apply additional styling

### Events

* **wf-change**: Triggered as users type (real-time) - payload contains the text area content
* **wf-change-finish**: Triggered when users finish editing (on blur) - payload contains the text area content

## Example

### Customer feedback form

This example shows how to create a customer feedback form with a text area for detailed comments.

**Interface:**

* Text Input for customer name
* Text area Input for feedback details
* Button to submit the form

**Text area Input configuration:**

* Label: "Your feedback"
* Placeholder: "Please share your experience with our product..."
* State element: `customer_feedback`
* Rows: 4

<img alt="Text area Input configuration" />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Placeholder</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Rows</td>
      <td>Number</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Text Input
Source: https://dev.writer.com/components/textinput



A user input component that allows users to enter single-line text values.

<img />

## Overview

The **Text Input** component allows users to enter single-line text values. It's the most basic form of user input and is essential for collecting information from users in your agent.

Text inputs automatically bind to state variables, making it easy to capture and use user input in your blueprint workflows. They support various styling options and can be configured for different use cases, including password fields.

## Common use cases

* **User information**: Collect names, emails, phone numbers, and other personal data
* **Search queries**: Allow users to search through data or content
* **Configuration values**: Let users set preferences or parameters
* **Form fields**: Collect data as part of larger forms
* **Password entry**: Secure input for sensitive information

## How it works

1. **Label**: Provide a clear label for the input field
2. **Placeholder**: Add helpful placeholder text to guide users
3. **Binding**: Connect to a state variable to store the input value
4. **Events**: Trigger workflows when users type or finish editing
5. **Styling**: Customize appearance with colors and CSS classes, and add a password mode for sensitive information

The component automatically updates the bound state variable as users type, and can trigger events for real-time processing or validation.

## Configuration options

### Basic settings

* **Label**: The field label displayed above the input
* **Placeholder**: Hint text shown when the field is empty
* **Link variable**: The name of the state variable that stores the input value

### Styling options

* **Password mode**: Toggle to hide input for sensitive data (default is disabled)
* **Accent color**: Set the focus color for the input field
* **Custom CSS classes**: Apply additional styling

### Events

* **wf-change**: Triggered as users type (real-time) - payload contains the input value
* **wf-change-finish**: Triggered when users finish editing (on blur) - payload contains the input value

## Example

### Search interface

This example shows how to create a search interface with a text input. The search query is stored in the `search_query` state variable. For the blueprint workflow, you can access the `search_query` state variable to see the user's query.

**Interface:**

* Text Input component for the search query
* Button component to trigger the search
* Results container to display matches

**Search text input configuration:**

* **Label**: "Search"
* **Placeholder**: "Enter search terms..."
* **Link variable**: `search_query`

<img alt="Text Input configuration" />

## Best practices

1. **Clear labels**: Use descriptive labels that clearly indicate what information is needed
2. **Helpful placeholders**: Provide examples or guidance in placeholder text
3. **Appropriate validation**: Validate input on both client and server side
4. **Real-time feedback**: Use the `wf-change` event for immediate user feedback
5. **Password security**: Use password mode for sensitive information

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Placeholder</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Password mode</td>
      <td>Boolean</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Time Input
Source: https://dev.writer.com/components/timeinput



A user input component that allows users to select a time.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Label</td>
      <td>Text</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-time-change" icon="code">
    Capture changes to this control.

    ```python theme={null}

    def onchange_handler(state, payload):

    # Set the state variable "new_time" to the new value, provided as a hh:mm string (in 24-hour format that includes leading zeros).

    state["new_time"] = payload
    ```
  </Accordion>
</AccordionGroup>

<events />


# Timer
Source: https://dev.writer.com/components/timer



A component that emits an event repeatedly at specified time intervals, enabling time-based refresh.

<img />

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Interval (ms)</td>
      <td>Number</td>
      <td>How much time to wait between ticks. A tick is considered finished when its event is handled.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Active</td>
      <td>Boolean</td>
      <td>Whether the timer should trigger tick events.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Accent</td>
      <td>Color</td>
      <td>-</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>

## Events

<AccordionGroup>
  <Accordion title="wf-tick" icon="code">
    Emitted when the timer ticks.

    ```python theme={null}
    def handle_timer_tick(state):

    # Increment counter when the timer ticks

    state["counter"] += 1
    ```
  </Accordion>
</AccordionGroup>

<events />


# Video Player
Source: https://dev.writer.com/components/videoplayer



A Video player component that can play various video formats.

<img />

Use your app's static folder to serve videos directly. For example, `static/my_video.mp4`.

Alternatively, you can pack bytes or files in state:

`state[&quot;vid_b&quot;] = wf.pack_bytes(vid_bytes, &quot;video/mp4&quot;)`

`state[&quot;vid_f&quot;] = wf.pack_file(vid_file, &quot;video/mp4&quot;)`

Afterwards, you can reference the video using the syntax `@{vid_f}`.

## Fields

<table>
  <thead>
    <th>Name</th>
    <th>Type</th>
    <th>Description</th>
    <th>Options</th>
  </thead>

  <tbody>
    <tr>
      <td>Source</td>
      <td>Text</td>
      <td>The URL of the video file. Alternatively, you can pass a file via state.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Enable controls</td>
      <td>Boolean</td>
      <td>Display Video player controls.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Autoplay</td>
      <td>Boolean</td>
      <td>Autoplay the video when the component is loaded.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Loop</td>
      <td>Boolean</td>
      <td>Loop the video when it reaches the end.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Muted</td>
      <td>Boolean</td>
      <td>Mute the video by default.</td>

      <td>
        <span>-</span>
      </td>
    </tr>

    <tr>
      <td>Custom CSS classes</td>
      <td>Text</td>
      <td>CSS classes, separated by spaces. You can define classes in custom stylesheets.</td>

      <td>
        <span>-</span>
      </td>
    </tr>
  </tbody>
</table>


# Adobe Experience Manager connector
Source: https://dev.writer.com/connectors/adobe-experience-manager

Connect WRITER Agent to Adobe Experience Manager to manage AEM content, sites, pages, content fragments, assets, launches, and Cloud Manager infrastructure

This guide shows you how to configure the [Adobe Experience Manager](https://business.adobe.com/products/experience-manager/adobe-experience-manager.html) connector for WRITER Agent. After setting up this connector, WRITER Agent can manage AEM content, sites, pages, content fragments, assets, and launches with full CRUD operations, and access Cloud Manager programs, environments, pipelines, and repositories in read-only mode.

## Set up the Adobe Experience Manager connector

Configure the Adobe Experience Manager connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**.

<Note>
  Dynamic client registration means Adobe dynamically creates and manages
  OAuth credentials during the setup process. You don't need to create your own
  OAuth application or provide a client ID and client secret.
</Note>

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Adobe Experience Manager connector
3. Enter a profile name and description for the connector
4. Select who has access by default (all users or specific teams)
5. Select **Next** to proceed to connection configuration
6. Select **Dynamic client registration** as the connection type
7. Select which tools to enable for your agents
8. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Asana connector
Source: https://dev.writer.com/connectors/asana

Connect WRITER Agent to Asana for workspaces, tasks, projects, and workload insights

This guide shows you how to configure the [Asana](https://asana.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can access workspaces and team members, create, read, update, and delete tasks and subtasks, and retrieve task details, comments, dependencies, and project structure—enabling AI-driven task creation, project summaries, and workload insights.

## Set up the Asana connector

Configure the Asana connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Asana connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your Asana workspace.
* **Organization-managed OAuth**: Create your own Asana OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Create an Asana OAuth application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, create an MCP app in the Asana developer console:

1. Navigate to the [Asana developer console](https://app.asana.com/0/developer-console) and sign in

2. Select **Create new app**

3. Enter your app name (for example, "My MCP Client")

4. Select **MCP app** as the app type

5. Select **Create app**

6. In the left sidebar, select **OAuth** and add the Writer redirect URI to Redirect URLs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. In the left sidebar, select **Manage distribution**. Under **Distribution method**, choose **Specific workspaces** (and add at least one workspace) or **Any workspace**, then select **Save changes**

8. Navigate to **Basic information** and copy the **Client ID** and **Client secret** for use in [AI Studio](#configure-the-connector-in-ai-studio)

For more information, see [Integrating with Asana's MCP Server](https://developers.asana.com/docs/integrating-with-asanas-mcp-server) documentation.

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Asana connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Atlassian connector
Source: https://dev.writer.com/connectors/atlassian

Connect WRITER Agent to Atlassian to access Jira and Confluence content and manage work items and pages

This guide shows you how to configure the [Atlassian](https://www.atlassian.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can search and read Jira issues and Confluence pages, create and update Jira work items, and create and edit Confluence pages.

## Set up the Atlassian connector

Configure the Atlassian connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**.

<Note>
  Dynamic client registration means Atlassian dynamically creates and manages
  OAuth credentials during the setup process. You don't need to create your own
  OAuth application or provide a client ID and client secret.
</Note>

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Atlassian connector
3. Enter a profile name and description for the connector
4. Select who has access by default (all users or specific teams)
5. Select **Next** to proceed to connection configuration
6. Select **Dynamic client registration** as the connection type
7. Select which tools to enable for your agents
8. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Contentful connector
Source: https://dev.writer.com/connectors/contentful

Connect WRITER Agent to Contentful to manage content entries, assets, and locales across your Contentful spaces

This guide shows you how to configure the [Contentful](https://www.contentful.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can publish and archive content entries, list locales, and perform other content management operations across your Contentful spaces.

## Set up the Contentful connector

Configure the Contentful connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**.

<Note>
  Dynamic client registration means Contentful dynamically creates and manages
  OAuth credentials during the setup process. You don't need to create your own
  OAuth application or provide a client ID and client secret.
</Note>

### Configure the connector in AI Studio

Create the connector profile and connection settings in AI Studio:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Search for **Contentful** and select the connector
3. On the **Profile** step, enter a profile name and description for the connector, and select who has access by default (all users or specific teams)
4. Select **Next** to continue to the **Type** step
5. Select the connection level: user level (each user authenticates their own account) or org level (shared connection to a single account)
6. Select **Dynamic client registration** as the **Managed by** option
7. Select **Next** to continue to the **Tools** step
8. Select which tools to enable for your agents, then select **Save**

### Authorize the connector in Contentful

When the OAuth authorization flow opens Contentful, grant access to your organization, spaces, and tool categories:

1. Log in to your Contentful account when prompted
2. Select **Authorize** to allow the Contentful Remote MCP application to access your content
3. Select the **Organization** where you install Contentful's Remote MCP Server
4. Under **Select spaces and environments**, select the **Space** and **Environment** to grant access to
5. Select **Add row** to grant access to additional spaces or environments
6. Under **Configure Contentful access**, select which tool categories the MCP server can access for the selected space and environment (for example, entries, assets, content types, environments, or AI Actions)
7. Choose read-only or read/write access for each category you enable
8. Expand **Content lifecycle actions** to configure entry-related actions like publishing and archiving
9. Select **I recognize and trust this URL** to confirm the redirect URL, then select **Agree** to authorize the WRITER MCP Client
10. Confirm you see **Connected to Contentful**, then close the tab

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Databricks connector
Source: https://dev.writer.com/connectors/databricks

Connect WRITER Agent to Databricks using MCP to access SQL, Unity Catalog functions, and Genie spaces

This guide shows you how to configure the [Databricks](https://www.databricks.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like querying Databricks SQL, accessing Unity Catalog functions, and interacting with Genie spaces through Databricks' managed MCP servers.

<Note>
  Databricks managed MCP servers are currently in Beta. Workspace administrators control access to this feature through the Databricks **Previews** page. If you don't see MCP servers available in your workspace, navigate to **Settings** → **Previews** and search for "Managed MCP Servers" to enable the feature. This feature is not available for free-tier accounts.
</Note>

## Set up the Databricks connector

Configure the Databricks connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Databricks connector uses OAuth to authenticate with Databricks-managed MCP servers.

<Note>
  The Databricks connector only supports organization-managed OAuth. WRITER-managed OAuth is not available for Databricks.
</Note>

### Obtain your Databricks MCP server URL

Before configuring the connector in AI Studio, locate your MCP server endpoint in your Databricks workspace:

1. Log in to your [Databricks workspace](https://accounts.cloud.databricks.com/)
2. Navigate to **AI/ML → Agents → MCP Servers**
3. Select the managed MCP server you want to connect (for example, SQL or Genie)
4. Copy the MCP server endpoint URL for use in [AI Studio](#configure-the-connector-in-ai-studio)

   The URL format depends on the type of MCP server:

   | Server type             | Example URL format                                                          |
   | ----------------------- | --------------------------------------------------------------------------- |
   | Vector search           | `https://<workspace-hostname>/api/2.0/mcp/vector-search/{catalog}/{schema}` |
   | DBSQL                   | `https://<workspace-hostname>/api/2.0/mcp/sql`                              |
   | Genie space             | `https://<workspace-hostname>/api/2.0/mcp/genie/{genie_space_id}`           |
   | Unity Catalog functions | `https://<workspace-hostname>/api/2.0/mcp/functions/{catalog}/{schema}`     |

For more details about Databricks managed MCP servers and how to get the IDs for specific server types, see [Databricks MCP documentation](https://docs.databricks.com/aws/en/generative-ai/mcp/managed-mcp).

### Create a Databricks OAuth application

Create an OAuth 2.0 application in Databricks:

1. Navigate to the [Databricks Account Console](https://accounts.cloud.databricks.com/)

2. Select **Settings** in the left navigation

3. Select **App connections**

4. Select **Add connection**

5. Enter an application name (for example, "Writer MCP connector")

6. Add the Writer redirect URI:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. Select **All APIs** under Access Scopes

8. Ensure **Generate a client secret** is selected

9. Set the access token TTL (for example, 10 minutes) and refresh token TTL (for example, 129600 minutes or 90 days)

10. Select **Create**

11. Copy the client ID and client secret for use in [AI Studio](#configure-the-connector-in-ai-studio)

For detailed instructions, see [Databricks OAuth documentation](https://docs.databricks.com/aws/en/generative-ai/mcp/connect-external-services#create-the-databricks-oauth-application).

### Configure the connector in AI Studio

After creating your Databricks OAuth application:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Databricks connector
3. Select who has access by default (all users or specific teams)
4. Enter your Databricks MCP server endpoint URL as the **URL**
5. Enter your OAuth client ID and client secret

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# FactSet connector
Source: https://dev.writer.com/connectors/factset

Connect WRITER Agent to FactSet to access financial data, equity fundamentals, and market analytics

This guide shows you how to configure the [FactSet](https://www.factset.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like accessing global equity fundamentals, earnings data, financial statements, estimates, and industry analytics.

## Set up the FactSet connector

Configure the FactSet connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The FactSet connector requires organization-managed OAuth authentication with private key JWT authentication method.

<Note>
  The FactSet connector only supports organization-managed OAuth. You must create your own FactSet OAuth application. WRITER-managed OAuth is not available for FactSet.
</Note>

### Create a FactSet OAuth application

Create an OAuth 2.0 application in FactSet:

1. Contact FactSet to enable OAuth 2.0 access for your account
2. Configure your OAuth application with FactSet and grant access to the [required OAuth scopes](#required-oauth-scopes)
3. Generate a private key for JWT authentication
4. Add the Writer redirect URI if required by FactSet
5. Note your client ID and authentication credentials

For detailed instructions, see [FactSet's OAuth 2.0 documentation](https://developer.factset.com/learn/authentication-oauth2).

#### Required OAuth scopes

* `api|content/eventsandtranscripts/v2` - Access events and transcripts
* `api|content/fundamentals` - Access fundamental financial data
* `api|content/estimates` - Access estimates and forecasts

<Note>
  OAuth scopes are fixed per connector and cannot be customized based on enabled tools. When users authorize the FactSet connector, they will grant all the scopes listed above, even if you disable certain tools in AI Studio.
</Note>

### Configure the connector in AI Studio

After creating your FactSet OAuth application:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the FactSet connector
3. Select who has access by default (all users or specific teams)
4. Select the connection level: User level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID
7. Upload or configure your private key for JWT authentication
8. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Gmail connector
Source: https://dev.writer.com/connectors/gmail

Connect WRITER Agent to Gmail to manage emails, search messages, and automate email workflows

This guide shows you how to configure the [Gmail](https://mail.google.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like listing email threads, retrieving messages, searching by filters and labels, drafting emails, and sending messages.

## Set up the Gmail connector

Configure the Gmail connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**.

<Note>
  The Gmail connector only supports organization-managed OAuth. You must create your own Gmail OAuth application. Writer-managed OAuth is not available for Gmail.
</Note>

<Note>
  Google OAuth credentials expire after 6 months of inactivity. If the Gmail connector is not used for 6 months, users will need to re-authenticate.
</Note>

### Create a Google OAuth application

Create a new Google OAuth application in the Google Cloud Console:

1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/)

2. Create a new project or select an existing project

3. Enable the Gmail API for your project

4. Configure the OAuth consent screen with your application details

5. Create OAuth 2.0 credentials (Web application type)

6. Add the Writer redirect URI to authorized redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. Configure the [required OAuth scopes](#required-oauth-scopes) for your application

8. Copy the client ID and client secret for use in AI Studio

For detailed instructions, see [Google's OAuth 2.0 documentation](https://support.google.com/cloud/answer/15549257).

#### Required OAuth scopes

* `https://www.googleapis.com/auth/gmail.labels` - View and manage labels
* `https://www.googleapis.com/auth/gmail.send` - Send email on the user's behalf
* `https://www.googleapis.com/auth/gmail.readonly` - View email messages and settings
* `https://www.googleapis.com/auth/gmail.compose` - Manage drafts and compose emails
* `https://www.googleapis.com/auth/gmail.insert` - Add emails to the mailbox
* `https://www.googleapis.com/auth/gmail.modify` - Modify email metadata
* `https://mail.google.com/` - Full Gmail access

<Note>
  OAuth scopes are fixed per connector and cannot be customized based on enabled tools. When users authorize the Gmail connector, they will grant all the scopes listed above, even if you disable certain tools in AI Studio.
</Note>

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Gmail connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Gong connector
Source: https://dev.writer.com/connectors/gong

Connect WRITER Agent to Gong to access call transcripts, meeting summaries, and sales analytics

This guide shows you how to configure the [Gong](https://www.gong.io/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like retrieving call transcripts, meeting summaries, performance scorecards, engagement metadata, and conversation analytics.

## Set up the Gong connector

Configure the Gong connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Gong connector supports API key authentication or OAuth 2.0 authentication (contact Gong to enable OAuth).

### Obtain Gong API credentials

For API key authentication:

1. Log in to your Gong account
2. Navigate to Settings > API
3. Create a new API key
4. Copy the API key for use in AI Studio
5. Note your Gong tenant URL (for example: `https://yourcompany.api.gong.io`)

For detailed instructions, see [Gong's API authentication guide](https://help.gong.io/docs/receive-access-to-the-api).

### Configure the connector in AI Studio

After obtaining your Gong API credentials:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Gong connector
3. Select who has access by default (all users or specific teams)
4. Select which tools to enable for your agents
5. Enter your API key credentials
6. Enter your Gong tenant URL (for example: `https://yourcompany.api.gong.io`)

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Google BigQuery connector
Source: https://dev.writer.com/connectors/google-bigquery

Connect WRITER Agent to Google BigQuery to securely query and analyze data in your BigQuery data warehouse

This guide shows you how to configure the [Google BigQuery](https://cloud.google.com/bigquery) connector for WRITER Agent. After setting up this connector, WRITER Agent can execute SQL queries, list datasets and tables, and retrieve metadata to surface data insights, answer analytical questions, and generate reports based on your BigQuery data warehouse.

## Set up the Google BigQuery connector

Configure the Google BigQuery connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Google BigQuery connector requires organization-managed OAuth authentication.

<Note>
  The Google BigQuery connector only supports organization-managed OAuth. You
  must create your own Google Cloud OAuth application. Writer-managed OAuth is
  not available for Google BigQuery.
</Note>

<Note>
  Google OAuth credentials expire after 6 months of inactivity. If the Google
  BigQuery connector is not used for 6 months, users will need to
  re-authenticate.
</Note>

### Enable the BigQuery MCP server

Before configuring the connector, enable the BigQuery API and MCP server in your Google Cloud project:

1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/)
2. Select your project or [create a new one](https://console.cloud.google.com/projectselector2)
3. [Enable the BigQuery API](https://console.cloud.google.com/apis/library/bigquery.googleapis.com) for your project (enabled by default for new projects)
4. Enable the BigQuery MCP server by running the following command in the [Google Cloud Shell](https://console.cloud.google.com/cloudshell) or your local terminal with the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) installed:
   ```bash theme={null}
   gcloud beta services mcp enable bigquery.googleapis.com \
       --project=PROJECT_ID
   ```
   Replace `PROJECT_ID` with your Google Cloud project ID.

<Note>
  After March 17, 2026, the BigQuery remote MCP server is automatically enabled
  when you enable BigQuery.
</Note>

For detailed instructions, see [Google's BigQuery MCP server documentation](https://docs.cloud.google.com/bigquery/docs/use-bigquery-mcp).

### Create a Google OAuth application

Create a new Google OAuth application in the Google Cloud Console:

1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/)

2. Select the same project where you enabled the BigQuery MCP server

3. Navigate to **APIs & Services** → **Credentials**

4. Configure the OAuth consent screen with your application details

5. Create OAuth 2.0 credentials (Web application type)

6. Add the Writer redirect URI to authorized redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. Configure the [required OAuth scopes](#required-oauth-scopes) for your application

8. Copy the client ID and client secret for use in [AI Studio](#configure-the-connector-in-ai-studio)

For detailed instructions, see [Google's OAuth 2.0 documentation](https://support.google.com/cloud/answer/15549257).

#### Required OAuth scopes

* `https://www.googleapis.com/auth/bigquery` - View and manage your data in BigQuery

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Google BigQuery connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools


# Google Calendar connector
Source: https://dev.writer.com/connectors/google-calendar

Connect WRITER Agent to Google Calendar to manage events, schedule meetings, and automate calendar workflows

This guide shows you how to configure the [Google Calendar](https://calendar.google.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like reading calendar events, creating new events, updating existing events, and managing calendar schedules.

## Set up the Google Calendar connector

Configure the Google Calendar connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Google Calendar connector requires organization-managed OAuth authentication.

<Note>
  The Google Calendar connector only supports organization-managed OAuth. You must create your own Google Calendar OAuth application. Writer-managed OAuth is not available for Google Calendar.
</Note>

<Note>
  Google OAuth credentials expire after 6 months of inactivity. If the Google Calendar connector is not used for 6 months, users will need to re-authenticate.
</Note>

### Create a Google OAuth application

Create a new Google OAuth application in the Google Cloud Console:

1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/)

2. Create a new project or select an existing project

3. Enable the Google Calendar API for your project

4. Configure the OAuth consent screen with your application details

5. Create OAuth 2.0 credentials (Web application type)

6. Add the Writer redirect URI to authorized redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. Configure the [required OAuth scopes](#required-oauth-scopes) for your application

8. Copy the client ID and client secret for use in AI Studio

For detailed instructions, see [Google's OAuth 2.0 documentation](https://support.google.com/cloud/answer/15549257).

#### Required OAuth scopes

* `https://www.googleapis.com/auth/calendar` - Full access to calendars
* `https://www.googleapis.com/auth/calendar.events` - Manage calendar events

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Google Calendar connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Google Docs connector
Source: https://dev.writer.com/connectors/google-docs

Connect WRITER Agent to Google Docs to read, edit, and manage documents

This guide shows you how to configure the [Google Docs](https://docs.google.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like retrieving document content, searching within documents, editing paragraphs and sections, and inserting new content while maintaining formatting.

## Set up the Google Docs connector

Configure the Google Docs connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Google Docs connector requires organization-managed OAuth authentication.

<Note>
  The Google Docs connector only supports organization-managed OAuth. You must create your own Google Docs OAuth application. Writer-managed OAuth is not available for Google Docs.
</Note>

<Note>
  Google OAuth credentials expire after 6 months of inactivity. If the Google Docs connector is not used for 6 months, users will need to re-authenticate.
</Note>

### Create a Google OAuth application

Create a new Google OAuth application in the Google Cloud Console:

1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/)

2. Create a new project or select an existing project

3. Enable the Google Docs API for your project

4. Configure the OAuth consent screen with your application details

5. Create OAuth 2.0 credentials (Web application type)

6. Add the Writer redirect URI to authorized redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. Configure the [required OAuth scopes](#required-oauth-scopes) for your application

8. Copy the client ID and client secret for use in AI Studio

For detailed instructions, see [Google's OAuth 2.0 documentation](https://support.google.com/cloud/answer/15549257).

#### Required OAuth scopes

* `https://www.googleapis.com/auth/documents` - Read and write access to Google Docs

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Google Docs connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Google Drive connector
Source: https://dev.writer.com/connectors/google-drive

Connect WRITER Agent to Google Drive to manage files, folders, and sharing across My Drive and shared drives

This guide shows you how to configure the [Google Drive](https://drive.google.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like listing and searching files, inspecting metadata, creating folders, copying or deleting items, exporting Google formats (PDF, DOCX, CSV), and managing sharing on My Drive and shared drives.

## Set up the Google Drive connector

Configure the Google Drive connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Google Drive connector requires organization-managed OAuth authentication.

<Note>
  The Google Drive connector only supports organization-managed OAuth. You must create your own Google Drive OAuth application. Writer-managed OAuth is not available for Google Drive.
</Note>

<Note>
  Google OAuth credentials expire after 6 months of inactivity. If the Google Drive connector is not used for 6 months, users will need to re-authenticate.
</Note>

### Create a Google OAuth application

Create a new Google OAuth application in the Google Cloud Console:

1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/)

2. Create a new project or select an existing project

3. Enable the [Google Drive API](https://console.cloud.google.com/marketplace/product/google/drive.googleapis.com) for your project

4. Configure the OAuth consent screen with your application details

5. Create OAuth 2.0 credentials (Web application type)

6. Add the Writer redirect URI to authorized redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. Configure the [required OAuth scopes](#required-oauth-scopes) for your application

8. Copy the client ID and client secret for use in AI Studio

For detailed instructions, see [Google's OAuth 2.0 documentation](https://support.google.com/cloud/answer/15549257).

#### Required OAuth scopes

* `https://www.googleapis.com/auth/drive` - Full access to Google Drive files and folders

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Google Drive connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Google Sheets connector
Source: https://dev.writer.com/connectors/google-sheets

Connect WRITER Agent to Google Sheets to read, update, and analyze spreadsheet data

This guide shows you how to configure the [Google Sheets](https://sheets.google.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like reading sheets and ranges, searching data using filters, writing or updating cells and rows, appending data, and performing batch edits.

## Set up the Google Sheets connector

Configure the Google Sheets connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Google Sheets connector requires organization-managed OAuth authentication.

<Note>
  The Google Sheets connector only supports organization-managed OAuth. You must create your own Google Sheets OAuth application. Writer-managed OAuth is not available for Google Sheets.
</Note>

<Note>
  Google OAuth credentials expire after 6 months of inactivity. If the Google Sheets connector is not used for 6 months, users will need to re-authenticate.
</Note>

### Create a Google OAuth application

Create a new Google OAuth application in the Google Cloud Console:

1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/)

2. Create a new project or select an existing project

3. Enable the Google Sheets API for your project

4. Configure the OAuth consent screen with your application details

5. Create OAuth 2.0 credentials (Web application type)

6. Add the Writer redirect URI to authorized redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

7. Configure the [required OAuth scopes](#required-oauth-scopes) for your application

8. Copy the client ID and client secret for use in AI Studio

For detailed instructions, see [Google's OAuth 2.0 documentation](https://support.google.com/cloud/answer/15549257).

#### Required OAuth scopes

* `https://www.googleapis.com/auth/spreadsheets` - Read and write access to Google Sheets

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Google Sheets connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# HubSpot connector
Source: https://dev.writer.com/connectors/hubspot

Connect WRITER Agent to HubSpot to access CRM data, contacts, deals, and pipeline insights

This guide shows you how to configure the [HubSpot](https://www.hubspot.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like retrieving and searching CRM records including contacts, companies, deals, tickets, engagements, and notes.

<Note>
  The HubSpot connector provides read-only access to CRM data. It does not support creating or updating records.
</Note>

## Set up the HubSpot connector

Configure the HubSpot connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The HubSpot connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your HubSpot account.
* **Organization-managed OAuth**: Create your own HubSpot OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Create a HubSpot OAuth application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, create an OAuth 2.0 application in HubSpot:

1. Navigate to your [HubSpot developer account](https://developers.hubspot.com/)

2. Create a new app or select an existing app

3. Configure OAuth settings with the [required scopes](#required-oauth-scopes)

4. Add the Writer redirect URI to Redirect URLs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

5. Copy the client ID and client secret

For detailed instructions, see [HubSpot's OAuth documentation](https://developers.hubspot.com/docs/api/oauth).

#### Required OAuth scopes

* `oauth` - Base OAuth access
* `crm.objects.tickets.read` - Read ticket data
* `crm.objects.users.read` - Read user information
* `crm.objects.products.read` - Read product catalog
* `crm.objects.line_items.read` - Read line item details
* `crm.objects.carts.read` - Read shopping cart data
* `crm.objects.subscriptions.read` - Read subscription information
* `crm.objects.owners.read` - Read object ownership data
* `crm.objects.orders.read` - Read order information
* `crm.objects.invoices.read` - Read invoice data
* `crm.objects.quotes.read` - Read quote information

<Note>
  OAuth scopes are fixed per connector and cannot be customized based on enabled tools. When users authorize the HubSpot connector, they will grant all the scopes listed above, even if you disable certain tools in AI Studio.
</Note>

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the HubSpot connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Microsoft Calendar connector
Source: https://dev.writer.com/connectors/microsoft-calendar

Connect WRITER Agent to Microsoft Calendar to manage events and schedules

This guide shows you how to configure the [Microsoft Calendar](https://www.microsoft.com/en-us/microsoft-365/outlook/calendar) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like accessing events, calendars, and scheduling information.

## Set up the Microsoft Calendar connector

Configure the Microsoft Calendar connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Microsoft Calendar connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your Microsoft Calendar.
* **Organization-managed OAuth**: Create your own Microsoft OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Create a Microsoft OAuth application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, first create a new Microsoft OAuth application in Microsoft Entra ID (Azure AD):

1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/)

2. Go to Applications > App registrations

3. Create a new registration

4. Configure authentication with Web platform

5. Add the Writer redirect URI to Redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Add API permissions for the [required Microsoft Graph scopes](#required-oauth-scopes)

7. Create a client secret in Certificates & secrets

8. Copy the application (client) ID and client secret

For detailed instructions, see [Microsoft's OAuth 2.0 documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow).

#### Required OAuth scopes

* `User.Read` - Read user profile information
* `Calendars.Read` - Read user calendars
* `Calendars.ReadWrite` - Read and write user calendars
* `offline_access` - Maintain access to data

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Microsoft Calendar connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Microsoft OneDrive connector
Source: https://dev.writer.com/connectors/microsoft-onedrive

Connect WRITER Agent to OneDrive to access and manage files and folders

This guide shows you how to configure the [Microsoft OneDrive](https://www.microsoft.com/en-us/microsoft-365/onedrive) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like accessing files and folders stored in OneDrive, reading file content, managing documents, and searching across cloud storage.

## Set up the Microsoft OneDrive connector

Configure the Microsoft OneDrive connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Microsoft OneDrive connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your OneDrive.
* **Organization-managed OAuth**: Create your own Microsoft OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Create a Microsoft OAuth application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, first create a new Microsoft OAuth application in Microsoft Entra ID (Azure AD):

1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/)

2. Go to Applications > App registrations

3. Create a new registration

4. Configure authentication with Web platform

5. Add the Writer redirect URI to Redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Add API permissions for the [required Microsoft Graph scopes](#required-oauth-scopes)

7. Create a client secret in Certificates & secrets

8. Copy the application (client) ID and client secret

For detailed instructions, see [Microsoft's OAuth 2.0 documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow).

#### Required OAuth scopes

* `Files.ReadWrite` - Read and write user files
* `User.Read` - Read user profile information
* `offline_access` - Maintain access to data

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Microsoft OneDrive connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Microsoft Outlook connector
Source: https://dev.writer.com/connectors/microsoft-outlook

Connect WRITER Agent to Microsoft Outlook to manage emails and communications

This guide shows you how to configure the [Microsoft Outlook](https://www.microsoft.com/en-us/microsoft-365/outlook) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like accessing emails, folders, contacts, and mail settings.

## Set up the Microsoft Outlook connector

Configure the Microsoft Outlook connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Microsoft Outlook connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your Microsoft Outlook.
* **Organization-managed OAuth**: Create your own Microsoft OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Create a Microsoft OAuth application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, first create a new Microsoft OAuth application in Microsoft Entra ID (Azure AD):

1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/)

2. Go to Applications > App registrations

3. Create a new registration

4. Configure authentication with Web platform

5. Add the Writer redirect URI to Redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Add API permissions for the [required Microsoft Graph scopes](#required-oauth-scopes)

7. Create a client secret in Certificates & secrets

8. Copy the application (client) ID and client secret

For detailed instructions, see [Microsoft's OAuth 2.0 documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow).

#### Required OAuth scopes

* `User.Read` - Read user profile information
* `Mail.ReadWrite` - Read and write access to user mail
* `Mail.Send` - Send mail as the user
* `offline_access` - Maintain access to data

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Microsoft Outlook connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Microsoft SharePoint connector
Source: https://dev.writer.com/connectors/microsoft-sharepoint

Connect WRITER Agent to SharePoint to access sites, documents, and collaboration content

This guide shows you how to configure the [Microsoft SharePoint](https://www.microsoft.com/en-us/microsoft-365/sharepoint) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like accessing SharePoint sites, reading documents and lists, searching content across your organization's SharePoint environment, and managing files stored in document libraries.

## Set up the Microsoft SharePoint connector

Configure the Microsoft SharePoint connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Microsoft SharePoint connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your SharePoint.
* **Organization-managed OAuth**: Create your own Microsoft OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Create a Microsoft OAuth application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, first create a new Microsoft OAuth application in Microsoft Entra ID (Azure AD):

1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/)

2. Go to Applications > App registrations

3. Create a new registration

4. Configure authentication with Web platform

5. Add the Writer redirect URI to Redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Add API permissions for the [required Microsoft Graph scopes](#required-oauth-scopes)

7. Create a client secret in Certificates & secrets

8. Copy the application (client) ID and client secret

For detailed instructions, see [Microsoft's OAuth 2.0 documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow).

#### Required OAuth scopes

For self-managed OAuth, choose one of the following scope configurations based on the level of access you need:

* **Option A — Broad access**: Add the following scopes to grant read and manage access across all site collections:
  * `Sites.Read.All` - Read items in all site collections
  * `Sites.Manage.All` - Create, edit, and delete items and lists in all site collections
  * `Files.ReadWrite.All` - Read and write files the user can access
  * `offline_access` - Maintain access to data

* **Option B — Selected sites only**: Add the following scopes, then have a tenant admin grant `FullControl` on specific sites through the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/permissions-selected-overview?tabs=http). This avoids granting access to your entire SharePoint environment.
  * `Sites.Selected` - Access selected site collections
  * `User.Read` - Read user profile information
  * `offline_access` - Maintain access to data

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Microsoft SharePoint connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Microsoft Teams connector
Source: https://dev.writer.com/connectors/microsoft-teams

Connect WRITER Agent to Microsoft Teams to access channels, messages, and team collaboration

This guide shows you how to configure the [Microsoft Teams](https://www.microsoft.com/en-us/microsoft-teams) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like accessing teams, channels, messages, and collaboration settings.

## Set up the Microsoft Teams connector

Configure the Microsoft Teams connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Microsoft Teams connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your Microsoft Teams.
* **Organization-managed OAuth**: Create your own Microsoft OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Create a Microsoft OAuth application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, first create a new Microsoft OAuth application in Microsoft Entra ID (Azure AD):

1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/)

2. Go to Applications > App registrations

3. Create a new registration

4. Configure authentication with Web platform

5. Add the Writer redirect URI to Redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Add API permissions for the [required Microsoft Graph scopes](#required-oauth-scopes)

7. Create a client secret in Certificates & secrets

8. Copy the application (client) ID and client secret

For detailed instructions, see [Microsoft's OAuth 2.0 documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow).

#### Required OAuth scopes

* `User.Read` - Read user profile information
* `Team.ReadBasic.All` - Read basic team information
* `Channel.ReadBasic.All` - Read basic channel information
* `ChannelMessage.ReadWrite` - Read and write channel messages
* `ChannelMessage.Read.All` - Read all channel messages
* `ChannelMessage.Send` - Send messages to channels
* `ChannelSettings.ReadWrite.All` - Read and write channel settings
* `Chat.Create` - Create chats
* `Chat.ReadBasic` - Read basic chat information
* `Chat.Read` - Read chat messages
* `Chat.ReadWrite` - Read and write chat messages
* `Chat.ReadWrite.All` - Read and write all chat messages
* `ChatMessage.Read` - Read chat messages
* `ChatMessage.Send` - Send chat messages
* `ChatMember.ReadWrite` - Manage chat members
* `Group.Read.All` - Read all groups
* `Group.ReadWrite.All` - Read and write all groups
* `TeamMember.ReadWrite.All` - Manage team members
* `offline_access` - Maintain access to data

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Microsoft Teams connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# PitchBook connector
Source: https://dev.writer.com/connectors/pitchbook

Connect WRITER Agent to PitchBook to access private market data and investment analytics

This guide shows you how to configure the [PitchBook](https://pitchbook.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like exploring private market data including company profiles, funding rounds, valuations, investor information, and deal histories.

## Set up the PitchBook connector

Configure the PitchBook connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The PitchBook connector uses API key authentication.

### Obtain a PitchBook API key

Create an API key in PitchBook:

1. Log in to your PitchBook account
2. Navigate to API settings or contact PitchBook support for API access
3. Generate a new API key
4. Copy the API key for use in AI Studio

For detailed instructions, see [PitchBook's API documentation](https://pitchbook.com/help/PitchBook-api).

### Configure the connector in AI Studio

After obtaining your PitchBook API key:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the PitchBook connector
3. Select who has access by default (all users or specific teams)
4. Select which tools to enable for your agents
5. Enter your PitchBook API key

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Salesforce connector
Source: https://dev.writer.com/connectors/salesforce

Connect WRITER Agent to Salesforce to query CRM data, manage records, run reports, and automate workflows

This guide shows you how to configure the [Salesforce](https://www.salesforce.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can query and manage CRM records, run reports, execute SOQL/SOSL searches, interact with Chatter, and automate Salesforce workflows.

## Set up the Salesforce connector

Configure the Salesforce connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Salesforce connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required—just authorize access to your Salesforce account.
* **Organization-managed OAuth**: Create your own Salesforce External Client Application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use organization-managed OAuth if you need custom branding or have specific security requirements.
</Note>

### Configure Salesforce

Before connecting, verify the following in your Salesforce environment:

1. Confirm that the Salesforce user account has API access enabled
2. Verify the user has appropriate permissions for the objects and operations you plan to use
3. Ensure any required Salesforce features (reports, Chatter, Bulk API) are enabled for your org

### Install the Writer app from Salesforce AgentExchange (WRITER-managed only)

If you choose WRITER-managed OAuth, install the Writer Salesforce Connector package in your Salesforce org from AgentExchange. A Salesforce admin must complete this step.

1. Go to the [Writer for Sales Personalization](https://appexchange.salesforce.com/appxListingDetail?listingId=68c1909f-4f6e-45cb-a07f-35afbeb421e5) listing on Salesforce AgentExchange

2. Select **Get It Now**

   <img alt="The Writer for Sales Personalization listing on Salesforce AgentExchange, with the Get It Now button in the left sidebar" />

3. Select the Salesforce account to install the package into, then select **Install in Production**

   <img alt="The Install in Production dialog, showing the connected Salesforce account and the Install in Production button" />

4. Review the installation details, select the checkbox to agree to the terms and conditions, then select **Confirm and Install**

   <img alt="The Confirm Installation Details dialog, showing the package version, subscription, and organization, with the Confirm and Install button" />

5. Log in to Salesforce when prompted to continue the installation

6. Select **Install for All Users**, then select **Install**

7. Wait for the installation to complete, then select **Done**

8. In Salesforce, go to **Setup** > **Installed Packages** and confirm that **Writer Salesforce Connector** appears in the list with a status of **Active**

   <img alt="The Installed Packages page in Salesforce Setup, showing Writer Salesforce Connector with an Active status" />

### Create a Salesforce External Client Application (organization-managed only)

If you choose to create a self-managed OAuth application to connect, create an External Client Application (ECA) in Salesforce:

1. In Salesforce Setup, search for **External** in Quick Find, select **External Client App Manager**, then select **New External Client App**

2. Complete the required fields in the **Basic Information** section

3. Expand **API (Enable OAuth Settings)** and select **Enable OAuth**

4. Add the Writer redirect URI to the **Callback URL**:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

5. Select the [required OAuth scopes](#required-oauth-scopes)

6. Configure [flow enablement settings](#flow-enablement-settings)

7. Configure [security settings](#security-settings)

8. Select **Create**

9. Go to the app's **Settings** > **OAuth Settings** and copy the **Consumer Key** and **Consumer Secret**

For detailed instructions, see [Salesforce's External Client Apps documentation](https://help.salesforce.com/s/articleView?id=xcloud.external_client_apps.htm).

#### Required OAuth scopes

* `api` - Manage user data via APIs
* `refresh_token`, `offline_access` - Perform requests at any time
* `openid` - Access unique user identifiers
* `sfap_api` - Access the Salesforce API Platform

#### Flow enablement settings

Under **Flow Enablement**, select the following options:

* Enable Client Credentials Flow
* Enable Authorization Code and Credentials Flow
* Enable Token Exchange Flow
* Require secret for Token Exchange Flow

#### Security settings

In the **Security** section, select the following options:

* Require secret for Web Server Flow
* Require secret for Refresh Token Flow
* Require Proof Key for Code Exchange (PKCE) extension for Supported Authorization Flows
* Enable Refresh Token Rotation
* Limit Idle Refresh Token Time-to-Live (TTL) to 30 Days
* Enforce Refresh Token IP Allowlist

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Salesforce connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Semrush connector
Source: https://dev.writer.com/connectors/semrush

Connect WRITER Agent to Semrush to access SEO, keyword, traffic, and competitive research data

This guide shows you how to configure the [Semrush](https://www.semrush.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can access Semrush data through the [Semrush MCP server](https://www.semrush.com/blog/what-is-mcp-connector/), including keyword research, domain analytics, backlink data, traffic analytics, and competitive insights.

## Set up the Semrush connector

Configure the Semrush connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**.

<Note>
  Dynamic client registration means Semrush dynamically creates and manages
  OAuth credentials during the setup process. You don't need to create your own
  OAuth application or provide a client ID and client secret.
</Note>

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Semrush connector
3. Enter a profile name and description for the connector
4. Select who has access by default (all users or specific teams)
5. Select **Next** to proceed to connection configuration
6. Select **Dynamic client registration** as the connection type
7. Select which tools to enable for your agents
8. Complete the OAuth authorization flow

After configuration, end users authenticate with their own Semrush account through WRITER Agent the first time they use the connector.

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Slack connector
Source: https://dev.writer.com/connectors/slack

Connect WRITER Agent to Slack to search messages, manage channels, and automate team communication

This guide shows you how to configure the [Slack](https://slack.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can perform operations like searching messages and files, reading conversations, managing channels, viewing user profiles, and sending messages to Slack workspaces.

<Warning>
  Enroll in [Slack's MCP beta program](https://docs.slack.dev/ai/mcp-server/) to
  use this connector. Contact your Slack partner manager to request access.
</Warning>

## Set up the Slack connector

Configure the Slack connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Slack connector supports two authentication options:

* **WRITER-managed OAuth** (recommended): Writer provides the OAuth application. No setup required - just authorize access to your Slack workspace.
* **Organization-managed OAuth**: Create your own Slack OAuth application for custom branding and control.

<Note>
  Most users should choose WRITER-managed OAuth for faster setup. Only use
  organization-managed OAuth if you need custom branding or have specific
  security requirements.
</Note>

### Create a Slack OAuth application (organization-managed only)

Create an OAuth 2.0 application in Slack:

1. Navigate to the [Slack API Apps page](https://api.slack.com/apps)

2. Create a new app (select "From scratch")

3. Navigate to **OAuth & Permissions** in the sidebar

4. Under **Scopes**, locate the **User Token Scopes** section (not Bot Token Scopes) and add the [required scopes](#required-oauth-scopes)

5. Add the Writer redirect URI to Redirect URLs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Under **Advanced token security via token rotation**, enable **Token Rotation**

7. Navigate to **Basic Information**

8. Copy the client ID and client secret for use in [AI Studio](#configure-the-connector-in-ai-studio)

For detailed instructions, see [Slack's OAuth documentation](https://api.slack.com/authentication/oauth-v2).

#### Required OAuth scopes

* `search:read.public` - Search public channel content
* `search:read.private` - Search private channel content
* `search:read.mpim` - Search multi-party direct messages
* `search:read.im` - Search direct messages
* `search:read.files` - Search files
* `search:read.users` - Search user information
* `chat:write` - Send messages as the user
* `channels:history` - View messages in public channels
* `mpim:history` - View messages in multi-party direct messages
* `im:history` - View messages in direct messages
* `canvases:read` - View Canvas documents
* `canvases:write` - Create and edit Canvas documents
* `users:read` - View users in the workspace
* `users:read.email` - View email addresses of users

<Note>
  OAuth scopes are fixed per connector and cannot be customized based on enabled
  tools. When users authorize the Slack connector, they will grant all the
  scopes listed above, even if you disable certain tools in AI Studio.
</Note>

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Slack connector
3. Select who has access by default (all users or specific teams)
4. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (shared connection to a single account)
   * **Managed by**: WRITER-managed or self-managed (your own OAuth app)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret (if using self-managed OAuth)
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Snowflake connector
Source: https://dev.writer.com/connectors/snowflake

Connect WRITER Agent to Snowflake to access SQL analytics, Cortex tools, object metadata, and semantic views

This guide shows you how to configure the [Snowflake](https://www.snowflake.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can translate natural-language queries into Snowflake calls across Cortex tools (Analyst, Search, Agent), SQL operations, object management, and Semantic Manager functions for querying semantic views, dimensions, and metrics.

<Warning>
  The Snowflake connector uses Snowflake's native role-based permissions. Users have the same permissions in Writer that they have in Snowflake. For example, if a user's Snowflake role allows write operations like `CREATE TABLE`, `INSERT`, `UPDATE`, or `DELETE`, they can perform those same operations through Writer.
</Warning>

## Set up the Snowflake connector

Configure the Snowflake connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Snowflake connector requires organization-managed OAuth authentication.

<Note>
  The Snowflake connector only supports organization-managed OAuth. You must create an MCP Server and configure OAuth in your Snowflake account. WRITER-managed OAuth is not available for Snowflake.
</Note>

### Create a Snowflake MCP Server

Set up an MCP Server and OAuth in Snowflake:

1. In the [Snowflake web interface](https://app.snowflake.com/), open a SQL worksheet and set the database and schema context where the MCP Server should live

2. Create the MCP Server:
   ```sql theme={null}
   CREATE MCP SERVER <server_name>
    FROM SPECIFICATION $$
      tools:
        - name: "sql_exec_tool"
          type: "SYSTEM_EXECUTE_SQL"
          title: "SQL Execution Tool"
          description: "Execute SQL queries against the connected Snowflake database"
        - name: "analyst_tool"
          type: "CORTEX_ANALYST_MESSAGE"
          title: "Cortex Analyst Tool"
          identifier: "<database>.<schema>.<semantic_view>"
          description: "Query data using natural language through Cortex Analyst"
        - name: "search_tool"
          type: "CORTEX_SEARCH_SERVICE_QUERY"
          title: "Cortex Search Tool"
          identifier: "<database>.<schema>.<search_service>"
          description: "Search unstructured data using Cortex Search"
    $$;
   ```
   Snowflake currently supports the following MCP tool types:
   * `SYSTEM_EXECUTE_SQL`: Execute SQL queries against Snowflake
   * `CORTEX_ANALYST_MESSAGE`: Query data using natural language through semantic views
   * `CORTEX_SEARCH_SERVICE_QUERY`: Search unstructured data using Cortex Search
   * `CORTEX_AGENT_RUN`: Invoke a Cortex Agent
   * `GENERIC`: Call custom user-defined functions or stored procedures

3. Grant the [required Snowflake privileges](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents-mcp#access-control) based on the MCP tools you exposed in step 2:
   ```sql theme={null}
    -- Allow the role to access the MCP Server
    GRANT USAGE ON MCP SERVER <server_name> TO ROLE <role_name>;

    -- Required for SYSTEM_EXECUTE_SQL
    GRANT USAGE ON WAREHOUSE <warehouse_name> TO ROLE <role_name>;
    GRANT USAGE ON DATABASE <database> TO ROLE <role_name>;
    GRANT USAGE ON SCHEMA <database>.<schema> TO ROLE <role_name>;
    GRANT SELECT ON ALL TABLES IN SCHEMA <database>.<schema> TO ROLE <role_name>;

    -- Required only if you expose Cortex tools
    -- Cortex Analyst (semantic views)
    GRANT SELECT ON SEMANTIC VIEW <database>.<schema>.<semantic_view> TO ROLE <role_name>;

    -- Cortex Search
    GRANT USAGE ON CORTEX SEARCH SERVICE <database>.<schema>.<search_service> TO ROLE <role_name>;
   ```

4. Create the [security integration](https://docs.snowflake.com/sql-reference/sql/create-security-integration-oauth-snowflake) with the Writer redirect URI and refresh token support:

   ```sql theme={null}
    CREATE OR REPLACE SECURITY INTEGRATION <integration_name>
    TYPE = OAUTH
    OAUTH_CLIENT = CUSTOM
    ENABLED = TRUE
    OAUTH_CLIENT_TYPE = 'CONFIDENTIAL'
    OAUTH_REDIRECT_URI = 'https://app.writer.com/mcp/oauth/callback'
    OAUTH_ISSUE_REFRESH_TOKENS = TRUE;
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

5. Retrieve and copy your **client ID** and **client secret** for use in [AI Studio](#configure-the-connector-in-ai-studio):
   ```sql theme={null}
   SELECT SYSTEM$SHOW_OAUTH_CLIENT_SECRETS('<INTEGRATION_NAME>');
   ```

6. Note your **Snowflake MCP Server URL**, which combines your tenant URL and MCP Server path for use in [AI Studio](#configure-the-connector-in-ai-studio):
   ```
   https://<account_identifier>.snowflakecomputing.com/api/v2/databases/<database>/schemas/<schema>/mcp-servers/<mcp_server_name>
   ```

For detailed instructions, see [Snowflake's managed MCP server guide](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents-mcp).

### Configure the connector in AI Studio

After creating your Snowflake MCP Server and OAuth integration:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Snowflake connector
3. Select who has access by default (all users or specific teams)
4. Enter your Snowflake tenant URL (MCP Server URL), OAuth client ID, and client secret
5. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture
* [Snowflake-managed MCP server](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents-mcp): Learn about creating and configuring MCP servers in Snowflake


# Webflow connector
Source: https://dev.writer.com/connectors/webflow

Connect WRITER Agent to Webflow to manage sites, pages, and CMS content

This guide shows you how to configure the [Webflow](https://webflow.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can access your Webflow sites, pages, and CMS collections—enabling AI-driven content creation, updates, and site management.

## Set up the Webflow connector

Configure the Webflow connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**.

<Note>
  The Webflow connector only supports organization-managed OAuth. You must create your own Webflow OAuth application. Writer-managed OAuth is not available for Webflow.
</Note>

### Create a Webflow OAuth application

Create an OAuth application in the Webflow workspace settings:

1. Sign in to [Webflow](https://webflow.com/) and navigate to your workspace

2. Go to **Workspace settings** > **Apps & integrations**

3. Select the **Develop** tab and select **Create new app**

4. Enter your app name (for example, "Writer MCP Client") and a description

5. Add the Writer redirect URI to **Redirect URIs**:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Under **Scopes**, select the permissions your agents need (for example, `sites:read`, `pages:read`, `pages:write`, `cms:read`, `cms:write`)

7. Select **Create app**

8. Copy the **Client ID** and **Client secret** for use in [AI Studio](#configure-the-connector-in-ai-studio)

For more information, see [Webflow's OAuth documentation](https://developers.webflow.com/data/reference/oauth-app).

### Configure the connector in AI Studio

After creating your Webflow OAuth application:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select the Webflow connector
3. Select who has access by default (all users or specific teams)
4. Select the connection level: user level (each user authenticates their own account) or org level (shared connection to a single account)
5. Select which tools to enable for your agents
6. Enter your OAuth client ID and client secret
7. Complete the OAuth authorization flow

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Wrike connector
Source: https://dev.writer.com/connectors/wrike

Connect WRITER Agent to Wrike to manage tasks, spaces, contacts, and comments across your Wrike account

This guide shows you how to configure the [Wrike](https://www.wrike.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can retrieve tasks, spaces, contacts, and task comments to help manage your Wrike projects.

## Set up the Wrike connector

Configure the Wrike connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The Wrike connector requires organization-managed OAuth—you must create your own Wrike OAuth application to connect.

### Create a Wrike OAuth application

Create an OAuth 2.0 app in Wrike:

1. Go to your Wrike profile, select **Apps & Integrations**, then go to the **API** tab

2. Create a new app and enter an **App name** and **App description**

3. Add the Writer redirect URI to the app's redirect URLs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

4. Create the app

5. Copy the **OAuth Client ID** and **Secret key**—you'll need these when connecting the connector in [AI Studio](#configure-the-connector-in-ai-studio)

For detailed instructions, see [Wrike's OAuth 2.0 documentation](https://developers.wrike.com/docs/oauth-20-authorization#initial-setup).

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio), search for **Wrike MCP**, and select the connector
2. Select who has access by default (all teams or specific teams)
3. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (one OAuth connection shared across your organization)
   * **Managed by**: Self managed (your own OAuth app)
4. Select which tools can be used by default
5. Confirm the redirect URL matches the one you added to your Wrike OAuth app, then enter your OAuth client ID and client secret
6. Complete the OAuth authorization flow to connect to Wrike

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# ZoomInfo connector
Source: https://dev.writer.com/connectors/zoominfo

Connect WRITER Agent to ZoomInfo to research companies and contacts, and access intent and signal data

This guide shows you how to configure the [ZoomInfo](https://www.zoominfo.com/) connector for WRITER Agent. After setting up this connector, WRITER Agent can look up reference data, enrich company signals, and access intent data to support your go-to-market research.

## Set up the ZoomInfo connector

Configure the ZoomInfo connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The ZoomInfo connector requires organization-managed OAuth—you must create your own MCP app in the ZoomInfo Developer Portal to connect.

<Note>
  Creating an MCP app requires a DevPortal subscription, assigned by your ZoomInfo admin.
</Note>

### Create a ZoomInfo MCP app

Create an MCP app in the ZoomInfo Developer Portal:

1. Go to the [ZoomInfo Developer Portal](https://developer.zoominfo.com/) and sign in, or open the **API/MCP** tab in the left navigation of the ZoomInfo app

2. Select **Create App**

3. Enter an app name (this name appears on the consent screen users see when they sign in)

4. Select **MCP App** as the app type—this automatically assigns the required data scopes, so no manual scope selection is needed

5. Add the Writer redirect URI to the app's redirect URIs:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

6. Select **Create**

7. Open the app and copy the **Client ID** and **Client Secret**—you'll need these when connecting the connector in [AI Studio](#configure-the-connector-in-ai-studio)

### Configure the connector in AI Studio

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio), search for **ZoomInfo**, and select the connector
2. Select who has access by default (all teams or specific teams)
3. Select the connection type:
   * **Level**: User level (each user authenticates their own account) or org level (one OAuth connection shared across your organization)
   * **Managed by**: Self managed (your own OAuth app)
4. Select which tools can be used by default
5. Confirm the redirect URL matches the one you added to your ZoomInfo MCP app, then enter your OAuth client ID and client secret
6. Complete the OAuth authorization flow to connect to ZoomInfo

## Next steps

* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable connectors in AI Studio
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Writer AI module
Source: https://dev.writer.com/framework/ai-module



This module leverages the [Writer Python SDK](https://pypi.org/project/writer-sdk/) to enable applications to interact with large language models (LLMs) in chat or text completion formats. It provides tools to manage conversation states and to dynamically interact with LLMs using both synchronous and asynchronous methods.

## Getting your API key

To utilize the Writer AI module, you'll need to configure the `WRITER_API_KEY` environment variable with an API key obtained from AI Studio. Here is a detailed [guide](/home/quickstart) to setup up this key. You will need to select an **API** app under **Developer tools**

Once you have your API key, set it as an environment variable on your system:

<CodeGroup>
  ```bash For macOS and Linux theme={null}
  export WRITER_API_KEY=your_api_key_here
  ```

  ```bash For Windows theme={null}
  set WRITER_API_KEY=your_api_key_here
  ```
</CodeGroup>

You can manage your environment variables using methods that best suit your setup, such as employing tools like [python-dotenv](https://pypi.org/project/python-dotenv/).

## Chat completion with the Conversation class

The `Conversation` class manages LLM communications within a chat framework, storing the conversation history and handling the interactions.

```python theme={null}
import writer as wf
import writer.ai

def handle_simple_message(state, payload):
    # Update the conversation state by appending the incoming user message.
    state["conversation"] += payload
    
    # Stream the complete response from the AI model in chunks.
    for chunk in state["conversation"].stream_complete():
        # Append each chunk of the model's response to the ongoing conversation state.
        state["conversation"] += chunk

# Initialize the application state with a new Conversation object.
initial_state = wf.init_state({
    "conversation": writer.ai.Conversation(),
})
```

### Initializing a conversation

A `Conversation` can be initialized with either a system prompt or a list of previous messages. It can also accept a default configuration dictionary that sets parameters for all interactions.

```python theme={null}
# Initialize with a system prompt for a Financial Analyst specializing in balance sheets
conversation = Conversation("You assist clients with analyzing and understanding their balance sheets")

# Initialize with a history of messages related to balance sheet queries
history = [
    {"role": "user", "content": "Can you explain the liabilities section?"},
    {"role": "assistant", "content": "Certainly! Liabilities are legally binding obligations payable to another entity."}
]

conversation = Conversation(history)

# Initialize with a configuration suitable for financial analysis discussions
config = {'max_tokens': 200, 'temperature': 0.5}
conversation = Conversation("You provide detailed insights into balance sheet components", config=config)
```

### Adding messages to conversation

Messages can be added to a `Conversation` instance using the `+` operator or the `add` method.

```python theme={null}
# Using the `+` operator to add a balance sheet-related query
conversation += {"role": "user", "content": "Can you break down the assets section of the balance sheet?"}

# Using the `add` method to add a balance sheet-related query
conversation.add(role="user", content="How should I interpret the equity section?")
```

### Completing and streaming Conversations

The `complete` and `stream_complete` methods facilitate interaction with the LLM based on the accumulated messages and configuration. These methods execute calls to generate responses and return them in the form of a message object, but do not alter the conversation's `messages` list, allowing you to validate or modify the output before deciding to add it to the history.

<CodeGroup>
  ```python complete theme={null}
  # Using `complete` to get a single response
  response = conversation.complete()
  print("LLM Response:", response)
  ```

  ```python stream_complete theme={null}
  # Using `stream_complete` to get streamed responses
  for chunk in conversation.stream_complete():
      print("Streamed Message:", chunk)
      # Manually adding to the conversation
      conversation += chunk
  ```
</CodeGroup>

Instance-wide configuration parameters can be complemented or overriden on individual call's level, if a `config` dictionary is provided to the method:

```python theme={null}
# Overriding configuration for a specific call
response = conversation.complete(config={'max_tokens': 200, 'temperature': 0.5})
```

### Using Graphs with Conversation

A `Graph` is a collection of files meant to provide their contents to the LLM during conversations. Framework allows you to create, retrieve, update, and delete graphs, as well as manage the files within them.

#### Creating and Managing Graphs

To create and manipulate graphs, use the following methods:

```python theme={null}
from writer.ai import create_graph, retrieve_graph, list_graphs, delete_graph

# Create a new graph
graph = create_graph(name="Financial Data", description="Quarterly reports")

# Retrieve an existing graph by ID
graph = retrieve_graph("d90a632b-5c1f-42b8-8748-5b7f769d9a36")

# Update a graph
graph.update(name="Updated Financial Data", description="Updated description")

# Retrieve a list of created graphs
graphs = list_graphs()
for graph in graphs:
    # Delete a graph
    delete_graph(graph)
```

#### Adding and Removing Files from Graphs

You can upload files, associate them with graphs, and download or remove them.

```python theme={null}
from writer.ai import upload_file

# Upload a file
file = upload_file(data=b"file content", type="application/pdf", name="Report.pdf")

# Add the file to a graph
graph.add_file(file)

# Remove the file from the graph
graph.remove_file(file)
```

#### Applying Graphs to Conversation completion

You can utilize graphs within conversations. For instance, you may want to provide the LLM access to a collection of files during an ongoing conversation to query or analyze the file content. When passing a graph to the conversation, the LLM can query the graph to retrieve relevant data.

```python theme={null}
# Retrieve a graph
graph = retrieve_graph("d90a632b-5c1f-42b8-8748-5b7f769d9a36")

# Pass the graph to the conversation for completion
response = conversation.complete(tools=graph)
```

Alternatively, you can define a graph using JSON:

```python theme={null}
tool = {
    "type": "graph",
    "graph_ids": ["d90a632b-5c1f-42b8-8748-5b7f769d9a36"]
}

response = conversation.complete(tools=tool)
```

### Using Function Calls with Conversations

<Warning>
  Function tools are only available with `palmyra-x4` and `palmyra-x5` models.
</Warning>

Framework allows you to register Python functions that can be called automatically during conversations. When the LLM determines a need for specific information or processing, it issues a request to use the local code (your function), and Framework handles that request automatically.

#### Defining Function Tools

Function tools are defined using either a Python class or a JSON configuration.

```python theme={null}
from writer.ai import create_function_tool

# Define a function tool with Python callable
def calculate_interest(principal: float, rate: float, time: float):
    return principal * rate * time

tool = create_function_tool(
    name="calculate_interest",
    callable=calculate_interest,
    parameters={
        "principal": {"type": "float", "description": "Loan principal"},
        "rate": {"type": "float", "description": "Interest rate"},
        "time": {"type": "float", "description": "Time in years"}
    }
)

response = conversation.complete(tools=tool)
```

Alternatively, you can define a function tool in JSON format, but the callable function must still be passed:

```python theme={null}
tool = {
    "type": "function",
    "name": "calculate_interest",
    "callable": calculate_interest,
    "parameters": {
        "principal": {"type": "float", "description": "Loan principal"},
        "rate": {"type": "float", "description": "Interest rate"},
        "time": {"type": "float", "description": "Time in years"}
    }
}

response = conversation.complete(tools=tool)
```

Function tools require the following properties:

* **`name: str`**: A string that defines how the function is referenced by the LLM. It should describe the function’s purpose.
* **`callable: Callable`**: A Python function that will be called automatically when needed by the LLM.
* **`parameters: dict`**: A dictionary that specifies what input the function expects. The keys should match the function’s parameter names, and each parameter should have a `type`, and an optional `description`.\
  Supported types are: `string`, `number`, `integer`, `float`, `boolean`, `array`, `object` and `null`.

#### Automated Function Calling

When a conversation involves a tool (either a graph or a function), Framework automatically handles the requests from LLM to use the tools during interactions. If the tool needs multiple steps (for example, querying data and processing it), Framework will handle those steps recursively, calling functions as needed until the final result is returned.

By default, to prevent endless recursion, Framework will only handle 5 consecutive tool calls. You can expand it in case it doesn't suit your case – both `complete()` and `stream_complete()` accept a `max_tool_depth` parameter, which configures the maximum allowed recursion depth:

```python theme={null}
response = conversation.complete(tools=tool, max_tool_depth=7)
```

### Providing a Tool or a List of Tools

You can pass either a single tool or a list of tools to the `complete()` or `stream_complete()` methods. The tools can be a combination of FunctionTool, Graph, or JSON-defined tools.

```python theme={null}
from writer.ai import create_function_tool, retrieve_graph

# Define a function tool
tool1 = create_function_tool(
    name="get_data",
    callable=lambda x: f"Data for {x}",
    parameters={"x": {"type": "string", "description": "Input value"}}
)

# Retrieve a graph
graph = retrieve_graph("d90a632b-5c1f-42b8-8748-5b7f769d9a36")

# Provide both tools in a list
response = conversation.complete(tools=[tool1, graph])
```

## Text generation without a conversation state

### Text generation against a string prompt

`complete` and `stream_complete` methods are designed for one-off text generation without the need to manage a conversation state. They return the model's response as a string. Each function accepts a `config` dictionary allowing call-specific configurations.

<CodeGroup>
  ```python complete theme={null}
  # Using `complete` for a single completion
  text_response = complete("Explore the benefits of AI.", config={'temperature': 0.3})
  print("Completion:", text_response)
  ```

  ```python stream_complete theme={null}
  # Using `stream_complete` for streamed text completions
  for text_chunk in stream_complete("Explore the benefits of AI.", config={'temperature': 0.3}):
      print("Streamed Text:", text_chunk)
  ```
</CodeGroup>

### Text generation against graphs

The `ask` and `stream_ask` methods allow you to query one or more graphs to generate responses from the information stored within them.

#### Two approaches to questioning graphs

There are two ways to query graphs, depending on your needs:

1. **Graph-Level Methods** (`Graph.ask`, `Graph.stream_ask`): Used when working with a single graph instance. These methods are tied directly to the Graph object, encapsulating operations within that instance.
2. **Module-Level Methods** (`writer.ai.ask`, `writer.ai.stream_ask`): Designed for querying multiple graphs simultaneously. These methods operate on a broader scale, allowing mixed inputs of graph objects and IDs.

<Note>
  •	Use graph-level methods when working with a single graph instance.
  •	Use module-level methods when querying multiple graphs or when graph IDs are your primary input.
</Note>

#### Parameters

Both methods include:
• `question: str`: The main query for the LLM.
• *Optional* `subqueries: bool` (default: `False`): Allows the LLM to generate additional questions during response preparation for more detailed answers. Enabling this might increase response time.

Method-level methods require:
• `graphs_or_graph_ids: list[Graph | str]`: A list of graphs to use for the question. You can pass `Graph` objects directly into the list, use graph IDs in string form, or a mix of both.

#### Graph-level methods

The graph-level methods, `Graph.ask` and `Graph.stream_ask`, are designed for interacting with a single graph. By calling these methods on a specific `Graph` instance, you can easily pose questions and retrieve answers tailored to that graph’s content.

<CodeGroup>
  ```python ask theme={null}
  # Retrieve a specific graph
  graph = retrieve_graph("f47ac10b-58cc-4372-a567-0e02b2c3d479")

  # Pose a question to the graph and get a complete response
  response = graph.ask("What are the benefits of renewable energy?")
  print(response)
  ```

  ```python stream_ask theme={null}
  # Retrieve a specific graph
  graph = retrieve_graph("f47ac10b-58cc-4372-a567-0e02b2c3d479")

  # Pose a question and stream the response in chunks
  for chunk in graph.stream_ask("Explain the history of solar energy."):
      print(chunk)
  ```
</CodeGroup>

#### Module-level methods

The module-level methods, `writer.ai.ask` and `writer.ai.stream_ask`, are designed for querying multiple graphs simultaneously. They are useful when you need to aggregate or compare data across multiple graphs.

<CodeGroup>
  ```python ask theme={null}
  from writer.ai import ask

  # Pose a question to multiple graphs
  response = ask(
      question="What are the latest advancements in AI?",
      graphs_or_graph_ids=[
          "550e8400-e29b-41d4-a716-446655440000",
          "123e4567-e89b-12d3-a456-426614174000"
          ]
  )
  print(response)
  ```

  ```python stream_ask theme={null}
  from writer.ai import stream_ask

  # Stream responses from multiple graphs
  for chunk in stream_ask(
      question="Describe the key features of renewable energy sources.",
      graphs_or_graph_ids=[
          "550e8400-e29b-41d4-a716-446655440000",
          "123e4567-e89b-12d3-a456-426614174000"
          ]
  ):
      print(chunk)
  ```
</CodeGroup>

## Using the `Tools` class

<Note>
  This document outlines the use of `Tools` in the Writer Framework; for more thorough documentation, check out (this guide)\[[https://dev.writer.com/tools](https://dev.writer.com/tools)].
</Note>

The `writer.ai.tools` instance provides access to Writer SDK `tools` resources, such as text splitting, medical content comprehension, and PDF parsing. Below is a guide on how to use each method.

### Splitting Content

The `split` method divides text into chunks based on a selected strategy.

```python theme={null}
from writer.ai import tools

content = \
    """
    This is a long piece of text that needs to be split into smaller parts.
    Lorem ipsum dolor sit amet...
    """
chunks = tools.split(content, strategy="llm_split")
print(chunks)
```

**Parameters**:

* `content` (str): The text to be split.
* `strategy` (str): The splitting strategy (`llm_split`, `fast_split`, or `hybrid_split`).

**Returns**:
A list of text chunks.

### Medical Content Comprehension

The `comprehend_medical` method processes medical text and extracts relevant entities based on a specified response type.

```python theme={null}
from writer.ai import tools

medical_text = "Patient shows symptoms of hypertension and diabetes."
entities = tools.comprehend_medical(medical_text, response_type="Entities")
print(entities)
```

**Parameters**:

* `content` (str): The medical text to process.
* `response_type` (str): The type of medical response (`Entities`, `RxNorm`, `ICD-10-CM`, or `SNOMED CT`).

**Returns**:
A list of extracted medical entities.

### PDF Parsing

The `parse_pdf` method extracts text content from a PDF file. The file can be referenced by its ID, or provided as a `File` object.

<CodeGroup>
  ```python file_id theme={null}
  from writer.ai import tools

  file_id = "example-file-id"
  parsed_content = tools.parse_pdf(file_id, format="text")
  print(parsed_content)
  ```

  ```python upload_file theme={null}
  from writer.ai import tools

  file_object = upload_file(
      data=pdf_content,
      type="application/pdf",
      name="uploaded_file.pdf"
  )
  parsed_content = tools.parse_pdf(file_object, format="text")
  print(parsed_content)
  ```
</CodeGroup>

**Parameters**:

* `file_id_or_file` (str or File): The file to parse (by ID or as an object).
* `format` (str): The format of the extracted content (`text` or `markdown`).

**Returns**:
The text content of the PDF.


# Application state
Source: https://dev.writer.com/framework/application-state



Each session is assigned a unique application state by the Framework.

## Initializing state

To set the initial application state, use the `wf.init_state()` method with a dictionary argument.

<Tip>
  All user sessions will start with a clone of this initial state.
</Tip>

```py theme={null}
import writer as wf

# Define the initial state
initial_state = wf.init_state({
    "counter": 0,
})

# Define an event handler that modifies the state
# It receives the session state as an argument and mutates it
def increment(state):
    state["counter"] += 1
```

In the above example, each session begins with a `counter` at 0. As users interact with the application and activate event handlers, their session's `counter` value will change. For instance, if a user triggers the `increment` handler three times, their counter will increase to 3.

To access the `counter` value in the Builder, use @.

### Managing nested state elements

To include nested elements in your state, use nested dictionaries:

```python theme={null}
# Example of nested state initialization
wf.init_state({
    "counter": 0,
    "my_app": {
        "title": "Nested value"
    }
})
```

You can reference nested elements in the Builder as `@{my_app.title}`.

### Backend-only state elements

By default, all of the elements in the session state are sent to the front-end.

<Warning>
  All state elements are transmitted to the front-end by default, regardless of their visibility in the user interface.
</Warning>

To keep certain state elements private (back-end-only), prefix them with an underscore `_`. This is useful in several scenarios:

1. When data synchronization to the front-end is unnecessary.
2. When data cannot be serialized for the front-end, such as database connections.
3. When data is sensitive to the specific session and should remain confidential.

These elements remain in the back-end and cannot be accessed from the Builder.

## Managing files and binary data

In components where the Builder interfaces with external data, such as images, it often requires the use of data URLs. The source for an *Image* component, for example, can be a standard URL or a data URL.

Packing Files and Binary Data: Files and binary data can be converted to data URLs before they are sent to the front-end. Use `wf.pack_file()` and `wf.pack_bytes()` for this purpose. The `mime_type` argument, while optional, specifies the media type, helping the browser to correctly handle the data.

```python theme={null}
import writer as wf

# Initialize state with various data types
wf.init_state({
    # Reference a file by its filesystem path
    "sales_spreadsheet": wf.pack_file("sales_spreadsheet.xlsx"),

    # Use a file-like object that implements a .read() method
    "main_image": wf.pack_file(image_file, mime_type="image/jpeg"),

    # Convert raw bytes specifying a MIME type
    "my_bytes": wf.pack_bytes(b"\x31\x33\x33\x37", mime_type="text/plain"),

    # Directly assign raw bytes without a MIME type
    "my_raw_bytes": b"\x31\x33\x33\x37",
})
```

## Handling non-standard data types

The front-end cannot directly display complex data types such as Pandas dataframes or Matplotlib figures. Such objects must be serialized before being sent.

<Tabs>
  <Tab title="Matplotlib figures">
    Matplotlib figures are converted to PNG data URLs, which can be shown using a standard *Image* component.

    ```python theme={null}
    wf.init_state({
        "my_matplotlib_fig": fig,
    })
    ```

    The element can be used in an *Image* component in the Builder by setting the source to `@{my_matplotlib_fig}`. Alternatively, as data inside a *File Download* component.
  </Tab>

  <Tab title="Plotly graphs">
    Plotly graphs are converted to Plotly JS specifications, using JSON. They can be used in *Plotly Graph* components.
  </Tab>

  <Tab title="Altair charts">
    Altair charts are converted to Vega Lite specifications, based on JSON. They can be used in *Vega Lite Chart* components.
  </Tab>

  <Tab title="Pandas dataframes">
    Pandas dataframes are converted to JSON and can be used in *Dataframe* components.
  </Tab>
</Tabs>

## State schema

State schema is a feature that allows you to define the structure of the state.
This is useful for ensuring that the state is always in the expected format.

Schema allows you to use features like

* typing checking with mypy / ruff
* autocomplete in IDEs
* declare dictionaries
* automatically calculate mutations on properties

more into [Advanced > State schema](./state-schema)

```python theme={null}
import writer as wf

class AppSchema(wf.WriterState):
    counter: int

initial_state = wf.init_state({
    "counter": 0
}, schema=AppSchema)

# Event handler
# It receives the session state as an argument and mutates it
def increment(state: AppSchema):
    state.counter += 1
```


# Authentication
Source: https://dev.writer.com/framework/authentication



The Writer Framework authentication module allows you to restrict access to your application. Framework will be able to authenticate a user through an identity provider such as Google, Microsoft, Facebook, Github, Auth0, etc.

<Warning>
  Authentication is done before accessing the application. It is not possible to
  trigger authentication for certain pages exclusively.
</Warning>

<Warning>
  Static assets from Writer Framework exposed through `/static` and `/extensions` endpoints are not protected behind Authentication.
</Warning>

## Use Basic Auth

Basic Auth is a simple authentication method that uses a username and password. Authentication configuration is done in the [server\_setup.py module](/framework/custom-server).

<Warning>
  Password authentication and Basic Auth are not sufficiently secure for critical applications. If HTTPS encryption fails, a user could potentially intercept passwords in plaintext. Additionally, these methods are vulnerable to brute force attacks that attempt to crack passwords. To enhance security, it is advisable to implement authentication through trusted identity providers such as Google, Microsoft, Facebook, GitHub, or Auth0.
</Warning>

```python server_setup.py theme={null}
import os
import writer.serve
import writer.auth

auth = writer.auth.BasicAuth(
    login=os.getenv('LOGIN'),
    password=os.getenv('PASSWORD'),
)

writer.serve.register_auth(auth)
```

### Brute force protection

A simple brute force protection is implemented by default. If a user fails to log in, the IP of this user is blocked.
Writer framework will ban the IP from either the `X-Forwarded-For` header or the `X-Real-IP` header or the client IP address.

When a user fails to log in, they wait 1 second before they can try again. This time can be modified by
modifying the value of `delay_after_failure`.

<img alt="429" />

## Use OIDC provider

Authentication configuration is done in the `server_setup.py` [module](/framework/custom-server). The configuration depends on your identity provider.
Here is an example configuration for Google.

<img alt="Authentication OIDC Principle" />

```python server_setup.py theme={null}
import os
import writer.serve
import writer.auth

oidc = writer.auth.Oidc(
    client_id="1xxxxxxxxx-qxxxxxxxxxxxxxxx.apps.googleusercontent.com",
    client_secret="GOxxxx-xxxxxxxxxxxxxxxxxxxxx",
    host_url=os.getenv('HOST_URL', "http://localhost:5000"),
    url_authorize="https://accounts.google.com/o/oauth2/auth",
    url_oauthtoken="https://oauth2.googleapis.com/token",
    url_userinfo='https://www.googleapis.com/oauth2/v1/userinfo?alt=json'
)

writer.serve.register_auth(oidc)
```

### Use pre-configured OIDC

The Writer Framework provides pre-configured OIDC providers. You can use them directly in your application.

| Provider | Function             | Description                                                                             |
| -------- | -------------------- | --------------------------------------------------------------------------------------- |
| Google   | `writer.auth.Google` | Allow your users to login with their Google Account                                     |
| Github   | `writer.auth.Github` | Allow your users to login with their Github Account                                     |
| Auth0    | `writer.auth.Auth0`  | Allow your users to login with different providers or with login password through Auth0 |

#### Google

You have to register your application into [Google Cloud Console](https://console.cloud.google.com/).

```python server_setup.py theme={null}
import os
import writer.serve
import writer.auth

oidc = writer.auth.Google(
	client_id="1xxxxxxxxx-qxxxxxxxxxxxxxxx.apps.googleusercontent.com",
	client_secret="GOxxxx-xxxxxxxxxxxxxxxxxxxxx",
	host_url=os.getenv('HOST_URL', "http://localhost:5000")
)

writer.serve.register_auth(oidc)
```

#### Github

You have to register your application into [Github](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app#registering-a-github-app)

```python server_setup.py theme={null}
import os
import writer.serve
import writer.auth

oidc = writer.auth.Github(
	client_id="xxxxxxx",
	client_secret="xxxxxxxxxxxxx",
	host_url=os.getenv('HOST_URL', "http://localhost:5000")
)

writer.serve.register_auth(oidc)
```

#### Auth0

You have to register your application into [Auth0](https://auth0.com/).

```python server_setup.py theme={null}
import os
import writer.serve
import writer.auth

oidc = writer.auth.Auth0(
	client_id="xxxxxxx",
	client_secret="xxxxxxxxxxxxx",
	domain="xxx-xxxxx.eu.auth0.com",
	host_url=os.getenv('HOST_URL', "http://localhost:5000")
)

writer.serve.register_auth(oidc)
```

### Authentication workflow

<img />

### App static assets

Static assets in your application are inaccessible. You can use the `app_static_public` parameter to allow their usage.
When `app_static_public` is set to `True`, the static assets in your application are accessible without authentication.

```python theme={null}
oidc = writer.auth.Auth0(
	client_id="xxxxxxx",
	client_secret="xxxxxxxxxxxxx",
	domain="xxx-xxxxx.eu.auth0.com",
	host_url=os.getenv('HOST_URL', "http://localhost:5000"),
	app_static_public=True
)
```

## User information in event handler

When the `user_info` route is configured, user information will be accessible
in the event handler through the `session` argument.

```python theme={null}
def on_page_load(state, session):
    email = session['userinfo'].get('email', None)
    state['email'] = email
```

## Unauthorize access

It is possible to reject a user who, for example, does not have the correct email address.

<Tip>
  You can also use userinfo inside app. You can restrict access to certain pages
  inside the application by using the `session` object. See [User information in
  event handler](#user-information-in-event-handler)
</Tip>

```python theme={null}
from fastapi import Request

import writer.serve
import writer.auth

oidc = ...

def callback(request: Request, session_id: str, userinfo: dict):
    if userinfo['email'] not in ['nom.prenom123@example.com']:
        raise writer.auth.Unauthorized(more_info="You can contact the administrator at <a href='https://support.example.com'>support.example.com</a>")

writer.serve.register_auth(oidc, callback=callback)
```

The default authentication error page look like this:

<img />

| Parameter    | Description            |
| ------------ | ---------------------- |
| status\_code | HTTP status code       |
| message      | Error message          |
| more\_info   | Additional information |

## Modify user info

User info can be modified in the callback.

```python theme={null}
from fastapi import Request

import writer.serve
import writer.auth

oidc = ...

def callback(request: Request, session_id: str, userinfo: dict):
	userinfo['group'] = []
	if userinfo['email'] in ['fabien@example.com']:
		userinfo['group'].append('admin')
		userinfo['group'].append('user')
	else:
		userinfo['group'].append('user')

writer.serve.register_auth(oidc, callback=callback)
```

## Custom unauthorized page

You can customize the access denial page using your own template.

```python theme={null}
import os

from fastapi import Request, Response
from fastapi.templating import Jinja2Templates

import writer.serve
import writer.auth

oidc = ...

def unauthorized(request: Request, exc: writer.auth.Unauthorized) -> Response:
    templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates"))
    return templates.TemplateResponse(request=request, name="unauthorized.html", status_code=exc.status_code, context={
        "status_code": exc.status_code,
        "message": exc.message,
        "more_info": exc.more_info
    })

writer.serve.register_auth(oidc, unauthorized_action=unauthorized)
```

## Enable in edit mode

Authentication is disabled in edit mode. To activate it, you must trigger the loading of the server\_setup module in edition mode.

```bash theme={null}
writer edit --enable-server-setup
```


# Backend-driven UI
Source: https://dev.writer.com/framework/backend-driven-ui



Framework facilitates backend-initiated user interface modifications. These changes are made possible through **Code-Managed Components** (CMCs), distinct from the *Builder-Managed Components* (BMCs).

CMCs, unlike BMCs, are dynamically created and modified via back-end code, and cannot be edited (but still can be viewed) within the application builder. It's important to also note that CMCs do not persist in your application's files and exist only during the application runtime, supporting dynamic UI adjustments.

<Note>
  To summarise:

  **CMC** – Code-Managed Component

  * created via **application back-end**;
  * **cannot be edited** in builder;
  * is **not saved** to `.wf/components-*.jsonl`.

  **BMC** – Builder-Managed Component

  * created via **builder**;
  * **can be edited** in builder;
  * is **saved** to `.wf/components-*.jsonl`.
</Note>

## UI manager

Framework provides two independent approaches for managing your application's UI: initializing a base UI and making session-specific updates.

### Initializing base UI

The `init_ui()` method sets up a UI manager to configure UI components at the application's startup. This creates a component set that is accessible across all sessions:

```python theme={null}
import writer as wf

with wf.init_ui() as ui:
    with ui.Page(id="my-page"):
        ui.Header({"text": "Hello World!"})
        ui.ColumnContainer(id="column-container")
```

### Making session-specific updates

For dynamic, session-specific UI updates, the `ui` parameter is used within handler functions. This approach allows for real-time modifications tailored to individual user sessions:

```python theme={null}
def display_user_data(ui, state):
    with ui.find("column-container"):
        with ui.Column():
            ui.Text({"text": f"And welcome {state["username"]}!"})
        with ui.Column():
            ui.Text({"text": f"Your data: {state["user_data"]}"})
```

## UI manager methods

### `find` method

You can use the `ui.find(component_id: str)` method to access existing components by ID:

```python theme={null}
with ui.find("column-container"):
    with ui.Column():
        ...
```

If the component couldn't be found, the method raises a `RuntimeError`.

### `refresh_with` method

You can use the `ui.refresh_with(component_id: str)` method to replace children CMCs of an existing component (referenced by its ID):

```python theme={null}
with ui.refresh_with("my-page"):
    # Previously existing children are cleared
    ui.Header({"text": "Hello New World!"})
    with ui.ColumnContainer():
        with ui.Column():
            ui.Text({"text": "Nobody here for now..."})
```

This method also allows to clear children CMCs of a component:

```python theme={null}
with ui.refresh_with("my-page"):
    # Empties the page
    pass
```

If a targeted component has builder-managed children, they will not be removed. A warning message will be recorded in the application's log for each BMC attempted to be removed. This does not stop the execution of the method – any remaining CMCs will still be removed.
As well as with `find` method, it also raises a `RuntimeError` if it fails to find a referenced component.

### `parent` method

`ui.parent(component_id: str, level: int = 1)` gives access to the id to parents at higher levels.

```python theme={null}
container = ui.parent('my-text') # first parent id

container = ui.parent('my-text', 3) # level 3 parent id
with ui.find(container):
	...
```

### Component methods

UI manager contains methods linked to each front-end component. For example, in previous code snippets we provide a `ui.Text` method, which is used for creating [Text components](https://dev.writer.com/components/text).

This method expects `content: dict` as first argument, which enables you to set the field properties of the component, through corresponding keys:

```python theme={null}
ui.Text(
    {
        "text": "Hello World!",  
        # The text content of the component
        "useMarkdown": "no",  
        # Will not use Markdown
        "alignment": "left",  
        # Text is aligned to the left
        "primaryTextColor": "#000000",  
        # The text color is black
        "cssClasses": "my-text hello-world"  
        # Apply 'my-text' and 'hello-world' CSS classes
    }
)
```

In a similar way, every other component method also expects `content` as its first argument:

```python theme={null}
ui.VideoPlayer(
    {
        "src": "https://example.com/assets/mov/rick-roll-video.mov",
        "autoplay": "yes",
        "controls": "no",
        "muted": "no",
        "loop": "no",
    }
)
```

In addition to `content`, a set of fields which is specific to the component type, you can also modify the base properties of the component itself, which are:

* **`id: str`**: A unique identifier used for accessing the component after it was created.\
  *Providing an identifier that is already taken would result in `RuntimeWarning` and the existing component being overwritten with a newly created one.*
  ```python theme={null}
  ui.Text(
      {"text": "Hello World!"}, 
      id="hello-world-text"
      )
  ```
  *If no ID is provided with a component, a UUID is automatically generated for it.*
  <Note>
    Make sure to provide an `id` if you intend to `find` the component later\
    As the `find` method relies on `id` of the component, retrieval might get tricky if its `id` was generated randomly.
  </Note>
* **`position: int`**: Determines the display order of the component in relation to its siblings.\
  Position `0` means that the component is the first child of its parent.\
  Position `-2` is used for components – such as [sidebars](https://dev.writer.com/components/sidebar) – that have a specific reserved position not related to their siblings.
  ```python theme={null}
  ui.Text(
      {"text": "Hello Parent, I'm your first child!"}, 
      position=0
      )
  ```
  *Position is calculated automatically for each component, and you should be careful when you override it with predefined value, as this might lead to unexpected results.*
* **`parentId: str`**: Determines the parent [container](#container-components) for the component. By default, components recognise the container in the context of which they were defined as their parent. This allows for linking components to their parents outside of context, or for overriding a parent within a context.
  ```python theme={null}
  ui.Text(
      {"text": "Hello Parent, I'm your child too!"}, 
      parentId="dear-parent"
      )
  ```
* **`visible: bool | str`**: Determines the visibility of the component, `True` by default.
  ```python theme={null}
  ui.Text({"text": "I'm visible!"}, visible=True)

  ui.Text({"text": "And I'm not!"}, visible=False)

  ui.Text({"text": "My visibility depends on the @{my_var}!"}, visible="my_var")
  ```
* **`handlers: dict[str, callable]`**: Attaches [event handlers](https://dev.writer.com/framework/event-handlers) to the component. Each dictionary key represents an event, and its value is the corresponding handler.:
  ```python theme={null}
  def increment(state):
      state["counter"] += 1

  initial_state = wf.init_state({"counter": 0})

  ...

  ui.Button(
      {"text": "My Counter: @{counter}"}, 
      handlers={"wf-click": increment}
      )
  # You have two options for adding a function 
  # to the `handlers` dictionary: 
  # directly pass the function itself, 
  # or use the function's name as a string. 
  # Both approaches yield the same outcome.
  ```
  *A component can be linked to multiple event handlers.*
* **`binding: dict[str, str]`**: Links the component to a state variable via [binding](https://dev.writer.com/framework/builder-basics#binding). The dictionary key is the bindable event, and the value is the state variable's name:
  ```python theme={null}
  initial_state = wf.init_state({
      "header_text": "Default Text"
      "counter": 0
      })

  ...

  ui.TextInput(
      {"label": "Bound Text"}, 
      binding={"wf-change": "header_text"}
      )
  # This input will display "Default Text"
  # Changing the text in this input will modify the `header_text` variable

  ui.SliderInput(
      {"minValue": 0, "maxValue": 300, "stepSize": 1}, 
      binding={"wf-number-change": "counter"}
      )
  # This slider will have 0 as a default value
  # Sliding it will modify the `counter` variable
  ```
  *Unlike handlers, a component can be linked to just one variable via a bindable event. If the `binding` dictionary includes multiple event-variable pairs, a `RuntimeError` will be triggered.*

### Container components

Framework provides multiple layout components that can serve as *containers* for other components.

You can use `with` keyword to define such layouts:

```python theme={null}
with ui.Section({"title": "My Section"}):
    ui.Text({"text": 'Hello World!'}, id="hello-world")
```

It also allows for "chaining" multiple containers together, creating extensive and deeply-nested layout structures when needed:

```python theme={null}
with ui.ColumnContainer(id="cmc-column-container"):
    with ui.Column(id="cmc-column-1"):
        with ui.Section({"title": "My Section 1"}):
            ui.Text({"text": 'Hello World!'}, id="hello-world-1")
    with ui.Column(id="cmc-column-2"):
        with ui.Section({"title": "My Section 2"}):
            ui.Text({"text": 'Hello World again!'}, id="hello-world-2")
```

<Warning>
  Most components depend on being inside of a container. This means, for example, that Text components in code above cannot be created as "orphans", outside a Column or Section. Attempting to do so would raise an `UIError`.
</Warning>

By default, components inside container's `with` are being *appended* to it:

```python theme={null}
with ui.Column(id="cmc-column-1"):
    ui.Text({"text": 'Hello World!'}, id="hello-world-1")

...

# Retrieves the Column component created before
with ui.find(id="cmc-column-1"): 
    # The following component is going to be appended 
    # to the retrieved Column
    ui.Text({"text": 'Hello World again!'}, id="hello-world-2")
```

This will result in a Column component having two children Text components. To replace or clear the children, use [`refresh_with` method](#refresh_with-method):

```python theme={null}
with ui.Column(id="cmc-column-1"):
    ui.Text({"text": 'Hello World!'}, id="hello-world-1")

...

with ui.refresh_with(id="cmc-column-1"):
    # The following component is going to replace 
    # previously existing children of the retrieved Column
    ui.Text(
        {"text": 'To Hello World, or not to Hello World?'}, 
        id="hello-world-new"
        )
```


# Backend-initiated actions
Source: https://dev.writer.com/framework/backend-initiated-actions



Targeted, backend-initiated actions can be triggered from event handlers, using methods of `state`. Internally, this is achieved using Framework's `mail`, ephemeral state that is cleared when it reaches the intended user.

## Triggering a file download

The `file_download` method takes the `data` and `file_name` arguments. The first must contain raw bytes (a `bytes` object) or a packed file. As mentioned in the [Application State](/framework/application-state#managing-files-and-binary-data) section of the guide, a packed file is obtained using the `wf.pack_file` or `wf.pack_bytes` methods.

```py theme={null}
def handle_file_download(state):
    # Pack the file as a FileWrapper object
    data = wf.pack_file("assets/story.txt", "text/plain")
    file_name = "thestory.txt"
    state.file_download(data, file_name)
```

## Adding a notification

<img alt="Notifications" />

Framework adds notifications when a runtime error takes place. You can add your own notifications using the `add_notification` method, which takes the `type`, `title` and `message` arguments. `type` must be one of `error`, `warning`, `info`, `success`.

```py theme={null}
def notify_of_things_that_happened(state):
    state.add_notification("error", "An Error", "Something bad happened.")
    state.add_notification("warning", "A Warning", "Be aware that something happened.")
    state.add_notification("info", "Some Info", "Something happened.")
    state.add_notification("success", "A Success", "Something good happened.")
```

## Opening a URL

Open a URL in a new tab using the `open_url` method, which takes the `url` argument.

```py theme={null}
def handle_open_website(state):
    state.open_url("https://writer.com")
```

The URL will be safely opened with `noopener` and `noreferrer` options.

<Warning>
  Popup blockers: Given that the URL is opened asynchronously, popup blockers will likely block the new window —unless the user has opted in.
</Warning>

## Changing the active page

The active page and route parameters can be changed using the methods `set_page` and `set_route_vars`. This is explained in more detail in [Page Routes](/framework/page-routes).


# Builder basics
Source: https://dev.writer.com/framework/builder-basics



Writer Framework Builder’s interface is an overlay on top of the running app, which allows you to edit your app while it’s running. This approach gives you an accurate representation of how the app will look and behave without the need to constantly preview it.

## Writer Framework Builder’s modes

Writer Framework Builder has two modes:

1. **UI mode:** This is the default mode, which provides an overlay of tools for building and editing the user interface. The app still runs in UI mode, allowing to you test its functionality while you’re building it. You can also edit the app’s code and view its log.

<img alt="Writer Framework Builder in UI mode" />

2. **Preview mode:** This mode lets you preview the application, experiencing it almost as your end users would. The tool overlay is hidden, but you can still edit the app’s code and view its log in this mode.

<img alt="Writer Framework Builder in Preview mode" />

You can switch between UI mode and Preview mode by using the **UI**/**Preview** selector near the upper left corner of the page.

<img alt="Writer Framework Builder’s UI/Preview selector" />

## Building your app in UI mode

### The different areas of UI mode

In UI mode, Writer Framework Builder’s page is divided into these areas:

<img alt="Writer Framework Builder in UI mode, with indivdual areas labeled" />

1. **Canvas:** This is where you lay out components to build your app’s user interface. It displays the UI as your users will see it.
2. **Core toolkit:** A “palette” of UI components that can be added to your app’s user interface. To add a component to your app’s UI, drag it onto the Canvas or into the Component tree.
3. **Component tree:** This provides an alternative way to view your app’s user interface: as a hierarchical tree structure. It’s useful for ensuring that UI components are located in the right place or in the correct container object, and is handy for selecting UI components in complex layouts.
4. **Component settings:** This panel lets you view and edit the settings for the currently selected UI component. You can hide the Component settings panel if you need more screen space to work on the Canvas.
5. **Top bar:** Contains the “high level” editing controls: switching between UI and Preview mode, undoing and redoing the most recent change to the UI, and viewing the application’s state.
6. **Bottom bar:** Contains the “low level” editing controls, which toggle the Code and Log panels.

### Defining your app’s user interface

Writer Framework Builder provides a selection of over 50 UI components that you can use to build your app’s user interface:

<img alt="Visual catalog of all the components in the Core toolkit" />

You define your app’s user interface by dragging components from the Core toolkit and dropping them either onto the Canvas or into the Component tree (remember, the Canvas and Component tree provide different ways to look at your app’s user interface). If you simply drag and drop a UI component onto an empty spot on the Canvas, it will be placed at the “end,” below all other UI components in the user interface. To place a UI component at a specific location, drag them over the desired location or parent component until you see the insertion lines.

<img alt="Writer Framework Builder with diagram showing how to drag and drop components from Core toolkit" />

It can sometimes be difficult to find the component you’re looking for, so the Core toolkit has a search field. You can use it to find the narrow down the list of components or find a specific one.

<img alt="Close-up of Core toolkit with instructions for using its Search text field" />

Some UI components, such as Section, can act as “parents,” which are UI components that can contain other UI components. Others, such as Text, cannot. Additionally, certain components have placement restrictions — for instance, a Column must be added to a Column Container, and a Sidebar can only be added to a Page.

<img alt="Writer Framework builder, showing parent-child relationship between a Section and the Text Input and Button components it contains" />

The Component tree provides a hierarchical view of your app’s user interface. It shows the top-down layout of UI components in your app, as well as the parent-child relationships between components, making it easier to understand your app’s structure and ensure that components are correctly nested.

You will find the Component tree useful when trying to reorganize the order of components in the UI, especially those located inside a parent UI component. For more flexibility and finer control, you can use the Component tree as a source or a destination for your drag and drop actions.

### Discovering components

The Builder is designed to allow easy discoverability of components. Rather than scouring specifications every time you need to use a component, you can rely on the visual editor to guide you.

1. **Short description:** You can hover on the component type to get a tooltip with a short description.
2. **Available properties and events:** Looking at *Settings* will allow you to see which of its properties are configurable.
3. **Built-in docs:** Components have short docs built into them. You can expand it by clicking the help icon in Settings.
4. **Event handler stub code:** Different events need to be handled differently. The built-in stub handlers, which can be found next to each event, can help you get started when writing event handlers.

### Selecting and editing components

To move or edit a component in your UI, you need to select it first. You can do this by clicking on the component either in the Canvas or in the Component tree. The selected component will be highlighted in both the Canvas and Component tree.

<img alt="Writer Framework Builder, with instructions to select a component by clicking on it either on the Canvas or in the Component tree" />

Selecting a UI component will allow you to view and edit its settings in the Component settings panel on the right side of the page.

<img alt="Writer Framework Builder, with Component settings panel on display, and instructions to hide the panel" />

To hide the Component settings panel to get a better view of the Canvas, click on **»** in the button bar. To show the panel, click on **⚙** in the button bar.

<img alt="Writer Framework Builder, with Component settings panel hidden, and instructions to show the panel" />

The settings in the Component settings panel are divided into the following sections:

<AccordionGroup>
  <Accordion title="Basic settings">
    <img alt="Component settings panel, with basic settings highlighted" />

    These settings are divided into two categores:

    1. **General**, which specifies the component’s content
    2. **Style**, which defines the component’s appearance.

    Values for these settings can include:

    1. Literals, e.g. `monkey`
    2. References to application state using the template syntax `@{}`, e.g. `@{my_favourite_animal}`.
    3. A combination of both, e.g. `My favourite animal is @{my_favourite_animal}`.
    4. Nested states can be accessed with `.` (dot), e.g. `@{building.height}`.
    5. Nested elements can be dynamically accessed with `[]`, e.g. `@{building[dynamic_prop]}` will be equivalent to `@{building.height}` when `dynamic_prop` equals `height`.

    The values for these settings can be of different types, such as *Text*, *Color* and *Number*. The values are stored as text and are cast to the correct type when evaluated.
  </Accordion>

  <Accordion title="Binding">
    <img alt="Component settings panel, with Binding settings highlighted" />

    Only input components have a **Binding** section, whose settings are used to bind the component to a variable in the application’s state. The binding is two-way; if the user changes the component’s value, the state variable will change to match, and any change the code makes to the bound state variable will also change the component’s value.

    For example, a *Slider Input* component can be bound to a state variable `my_var`. If the value of the slider changes, so does the value of `my_var`. Similarly, if the value of `my_var` changes, the slider is moved automatically to reflect the change.

    To bind an input component, specify the state element. For example, `my_var` or `building.height`. Note that this field should not contain the template syntax, e.g. `my_var` and not `@{my_var}`.
  </Accordion>

  <Accordion title="Events">
    <img alt="Component settings panel, with Events settings highlighted" />

    The **Events** section lists all the events generated by the selected component, with the option of setting event handlers for them. For example, one of the events that the Text Input component generates is the `wf-change` event, which occurs whenever the user changes the contents of the component.

    For more about event handlers, consult the [*Event handlers*](/framework/event-handlers) section of the Writer Framework documentation.
  </Accordion>

  <Accordion title="Visibility">
    <img alt="Component settings panel, with Visibility settings highlighted" />

    The **Visibility** settings control Whether the component should be displayed. There are three visibility options:

    1. **Yes**: The component is displayed.
    2. **No**: The component is *not* displayed. Note that hidden components are still part of the HTML code but aren't shown.
    3. **Custom**: The component’s visibility depends on the value of a given state or context element. For example, if set to `my_var`, visibility will depend on the value of the `my_var` state element. Note that this field, similarly to Binding, should only contain the state element, e.g. `my_var` and not `@{my_var}`.
  </Accordion>
</AccordionGroup>

### Component shortcuts

The Component shortcuts bar contains a set of options to perform various operations that are often performed on components.

<img alt="Component settings panel, with Component shortcuts bar highlighted" />

Options will be grayed out when they're not applicable to the relevant component. Most shortcuts can also be activated using the keyboard; hover the cursor over a shortcut to see its keyboard shortcut.

The shortcuts are:

<img alt="The Component shortcuts bar, with labels" />

* **Add**: Adds a child of a specified type to the selected component.
* **Move up**: Decrements the position index of the selected component, used to sort children within the parent container.
* **Move down**: Increments the position index of the selected component.
* **Cut**: Cuts the selected component and places it in the clipboard.
* **Copy**: Copies the selected component to the clipboard.
* **Paste**: Pastes the content of the internal clipboard using the selected component as a parent.
* **Go to parent**: Selects the parent of the selected component.
* **Delete**: Deletes the selected component.

### The Code editor and the Log

Writer Framework Builder provides a built-in Code editor for the `main.py` file, which defines the behavior of the app. You can toggle the Code panel by clicking the **Code** control near the lower left corner of the page:

<img alt="Writer Framework Builder, with the Code editor displayed" />

Any changes made to the code do not take effect until you click the **Save and run** button, located at the upper right corner of the code editor.

The log is a useful debugging tool that displays any messages sent to standard output via the `print()` function, as well as any error messages. You can toggle the Log panel by clicking the **Log** control near the lower right corner of the page:

<img alt="Writer Framework Builder, with the Log editor displayed" />

Note that both the Code editor and Log panes can be displayed at the same time:

<img alt="Writer Framework Builder, with the Code editor and Log displayed at the same time" />

## Testing your app in Preview mode

In Preview mode, the overlays that let you build the user interface — the Core toolkit, Component tree, and Component settings — are invisible and unavailable. You see the app *almost* as your users would see it; the Top bar and Bottom bar, which your users would not see, are still available to you in Preview mode.

<img alt="Writer Framework Builder in preview mode" />

You can still use the Code editor and Log in Preview mode:

<img alt="Writer Framework Builder in Preview mode, with the Code editor and Log displayed at the same time" />


# Chat assistant
Source: https://dev.writer.com/framework/chat-assistant



In this tutorial, you'll use the Writer Framework to create a simple yet powerful chat assistant that can engage in conversations on various topics, provide answers to your questions, and maybe even help you when you're experiencing writer's block!

The process will take only minutes using a drag-and-drop visual editor to build the user interface and Python for the back-end code.

Here's what the finished project will look like:

<img alt="Finished chat assistant project" />

## Prerequisites

Before starting, ensure you have:

* **A Writer account:** You don't need an account to use Writer Framework, but you'll need one to use the AI module. [Sign up for a free account here](https://app.writer.com/register).
* **Python 3.9.2 or later**: Use the installer from [python.org](https://www.python.org/downloads/).
* **pip:** This command-line application comes with Python and is used for installing Python packages, including those from Writer.
* **A basic understanding of Python:** You should be familiar with the basics of the language.
* **Your favorite code editor (optional):** There's a code editor built into Writer for editing back-end code, but you can also use Visual Studio Code, Notepad++, Vim, Emacs, or any text editor made for programming if you prefer.

## Setting up your project

### Create a Writer app and get its API key

First, you'll need to create a new app within Writer.

<Steps>
  <Step title="Create the app in Writer">
    Log into Writer. From the Home screen, click on the **Build an app** button.

    <img alt="Writer home screen" />

    The **Start building** menu will appear, presenting options for the types of apps you can create.

    Select **Framework**, located under **Developer tools**. This will create a brand new app based on Writer Framework.

    <img alt="&#x22;Start building&#x22; menu" />
  </Step>

  <Step title="Copy your app's API key">
    On the next screen, titled **How to deploy an application**, you can get the API key for the app by clicking on the **Reveal key** button, located under the text **Authenticate with an API key**. Your complete API key will be displayed, and a "copy" button will appear. Click this button to copy the key; you'll use it in the next step.

    <img alt="&#x22;How to deploy an application&#x22; page" />
  </Step>
</Steps>

### Set up your computer and create the app's project

The next step is to set up the Writer Framework environment on your computer. You'll do this by creating a directory for the project, installing dependencies, and creating the project for the application using a template.

<Steps>
  <Step title="Open your terminal application">
    Open your terminal application. On macOS and Linux, this application goes by the name *Terminal*; on Windows, you can use either *Windows PowerShell* (which is preferred) or *Command Prompt*.
  </Step>

  <Step title="Install the dependencies">
    <Note>If you already have the `writer` and `python-dotenv` packages installed on your computer, you can skip this step.</Note>

    Install the `writer` and `python-dotenv` packages by entering the following commands in your terminal application:

    ```
    pip install writer python-dotenv
    ```

    This command tells `pip`, the Python package installer, to install two packages:

    * `writer`, which provides some command-line commands and enables Python code to interact with Writer and the Writer Framework.
    * `python-dotenv`, which makes it easy to manage environment variables by loading them from a `.env` file. This one is optional for this exercise, but you might find it useful when working on larger projects.
  </Step>

  <Step title="Set the API key environment variable">
    To pass your API key to the Writer Framework, you need to set an environment variable called `WRITER_API_KEY`.

    Select your operating system and terminal application below, then copy and paste the command into your terminal application, replacing `[your_api_key]` with the API key you copied earlier:

    <CodeGroup>
      ```sh macOS/Linux (Terminal) theme={null}
      export WRITER_API_KEY=[your_api_key]
      ```

      ```sh On Windows (Windows PowerShell) theme={null}
      $env:WRITER_API_KEY=[your_api_key]
      ```

      ```sh On Windows (Command Prompt) theme={null}
      set WRITER_API_KEY=[your_api_key]
      ```
    </CodeGroup>

    The `WRITER_API_KEY` environment variable will remain defined as long your terminal session is open (that is, until you close your terminal application’s window).
  </Step>

  <Step title="Create the project">
    Create the project by entering this command into your terminal application:

    ```
    writer create chat-assistant --template=ai-starter
    ```

    This command sets up a new project called `chat-assistant` using a starter template called `ai-starter` so that you're not starting "from scratch."
  </Step>
</Steps>

## Build the UI

Now that you've created the project, it's time to define the UI. The Writer Framework's drag-and-drop capabilities make it easy — even if you haven't done much UI work before!

The project editor is a web application that runs on your computer and enables you to define and edit your app's user interface. Launch it by typing the following into your terminal application:

```
writer edit chat-assistant
```

You'll see a URL. Control-click it (command-click on macOS) to open it, or copy the URL and paste it into the address bar of a browser window.

The browser window will contain the project editor, which will look like this:

<img alt="Project editor" />

You'll see the following:

* The **canvas** is in the center. It displays the app's user interface.
* The column on the left contains:
  * The **Core toolkit**, which contains all the UI components. You define the user interface by dragging components from the Toolkit and placing them on the canvas.
  * The **Component tree**, which shows the arrangement of the UI components on the canvas. It's also useful for selecting items on the canvas, especially when it has a lot of UI components.

It's time to build the UI!

<Steps>
  <Step title="Examine the header">
    Select the **Header** component by clicking it — it's the component at the top, containing the title **AI STARTER** and a gray area labeled **Empty Header**.

    When you click it, you'll see the **properties** panel appear on the right side of the page. This lets you view and edit the properties of the selected component.

    <img alt="The selected header and its properties panel" />

    The first property you'll see in the panel is the **Text** property, which defines the text that appears as the header's title. It should contain the value `@{my_app.title}`. The `@{` and `}` indicate that `my_app.title` is a variable and that its contents should be the text displayed instead of the literal text "my\_app.title". You'll set the value of this variable soon.
  </Step>

  <Step title="Clear the Section's default title">
    Select the **Section** component by clicking it — it's just below the **Header** component and contains the title **Section Title** and a gray area labeled **Empty Section**.

    In the **properties** panel, clear out the value of the **Title** property. This will remove the **Section**'s default title.

    <img alt="The selected section and its properties panel" />
  </Step>

  <Step title="Add a Text component to the Section">
    Drag a **Text** component from the **Core toolkit** panel on the left (it's under **Content**, and you may need to scroll down a little to find it) and into the *Section*. Sections can act as containers for other components.

    <Note>You can search for a specific component by using the search bar at the top of the **Core toolkit** panel.</Note>

    Select the **Text** component. In the **properties** panel, set the **Text** property to provide instructions or context for your chat assistant. Here's an example: `Welcome to the Chat Assistant. Ask me anything!`

    <img alt="The text component and its properties panel" />
  </Step>

  <Step title="Add a Chatbot component to the Section">
    The heart of this app is the **Chatbot** component, a pre-built component that displays the conversation between the LLM and the user and provides a text field where the user can enter prompts.

    Drag a **Chatbot** component from the **Core toolkit** panel (it's under **Content**) into the *Section*, just below the Text box.

    <img alt="The chatbot component" />
  </Step>
</Steps>

## Add the back-end code

With the UI laid out, it's time to work on the logic behind it.

The logic behind the user interface is defined in a file named `main.py`, which is in your project's directory. This file was automatically generated; you'll update the code in it to define the behavior of your app.

The simplest way to edit `main.py` is within the project editor. Click on the "toggle code" button (beside the word **Code**) near the lower left corner of the project editor page.

<img alt="Project editor with arrow pointing to toggle code button" />

A pane with the name **Code** will appear at the bottom half of the screen, displaying an editor for the the contents of `main.py`.

<img alt="Project editor with the code editor displayed" />

<Note>If you'd rather use a code editor instead of coding in the browser, use it to open the `main.py` file in your project's directory.</Note>

Now follow these steps:

<Steps>
  <Step title="Import libraries and load the Writer Framework API key">
    You should see the following at the start of the file:

    ```python theme={null}
    import writer as wf
    import writer.ai
    ```

    Replace that code with the following:

    ```python theme={null}
    import os
    import writer as wf
    import writer.ai

    # Set the API key
    wf.api_key = os.getenv("WRITER_API_KEY")
    ```

    This code imports the libraries that the application will need and then reads your Writer Framework API key in the `WRITER_API_KEY` environment variable.
  </Step>

  <Step title="Create a handler to respond to the user's input">
    The application needs a function to handle incoming chat messages. Find these comments in the code...

    ```python theme={null}
    # Welcome to Writer Framework! 
    # This template is a starting point for your AI apps.
    # More documentation is available at https://dev.writer.com/framework
    ```

    ...and replace them with the following function:

    ```python theme={null}
    def generate_completion(state, payload):
      print(f"Here's what the user entered: {payload['content']}")
      state["conversation"] += payload
      print(f"Conversation: {state['conversation'].messages}")
      try:
          for index, chunk in enumerate(state["conversation"].stream_complete()):
              print(f"Chunk {index}: {chunk}")
              if not chunk.get("content"):
                  chunk["content"] = ""
              state["conversation"] += chunk
              
          print(f"state['conversation']:\n{state['conversation'].messages}")
      except Exception as e:
          print(f"Error during stream_complete: {e}")
    ```

    The `generate_completion()` function will be called when the user enters a prompt, which is contained in the `payload` object. The `payload` object is added to the `conversation` object contained in the application's `state`, which adds the user's prompt to the record of the conversation between the user and the LLM.

    After adding the user's prompt to the conversational record, `generate_completion()` calls the `conversation` object's `stream_complete()` method, which generates an LLM completion based on the conversation so far. As its name implies, `stream_complete()` returns the completion as a stream of text chunks, which are captured and added to the `conversation` object.

    <Note>The `conversation` object in the code above is an instance of Writer’s `Conversation` class. You can find out more about this class on our [*Writer AI module*](https://dev.writer.com/framework/ai-module) page.</Note>

    Note that `generate_completion()` completion uses a lot of `print()` functions for debugging purposes, and you can use them to get a better idea of what's happening in the function. You'll see their output in both your terminal application and in the project editor's 'log' pane (which will be covered shortly) as you use the chat assistant. This output will include:

    * The prompt the user entered
    * The chunks of data that make up the LLM's response as they are generated
    * The record of the conversation between the user and the LLM.

    The `print()` functions don't affect the operation of the chat assistant in any way, and you can remove them if you wish.
  </Step>

  <Step title="Initialize the application">
    The final step is to set the application's initial state. Find this code, which should be just after the `generate_completion()` function...

    ```python theme={null}
    # Initialise the state
    wf.init_state({
        "my_app": {
            "title": "AI STARTER"
        },
    })
    ```

    ...and replace it with this:

    ```python theme={null}
    # Initialize the state
    wf.init_state({
        "conversation": writer.ai.Conversation(),
        "my_app": {
            "title": "CHAT ASSISTANT"
        },
    })
    ```

    The Writer Framework's `init_state()` method sets the initial value of `state`, a dictionary containing values that define the state of the application. The key-value pairs in `state` are how you store values used by your app and how you pass data between the back-end code and the UI.

    The code above sets the initial value of `state` so that it has two key-value pairs:

    * `conversation`: An object that keeps a record of the conversation that the user is having with the LLM. You'll bind its value to the **Chatbot** component soon.
    * `my_app`: A dictionary containing values that define the application's appearance. This dictionary has a single key-value pair, `title`, which defines the text that appears as the application's title in the **Header**.

    <Note>For more details about the `state` variable, see our [*Application state*](https://dev.writer.com/framework/application-state#application-state) page.</Note>
  </Step>

  <Step title="Save the updated code and hide the code editor">
    That’s all the code. If you edited the code in the browser, save it by clicking the “save” button near the top right corner of the code editor.

    <img alt="Project editor and code editor, with arrow pointing to save button" />

    Click the "toggle code" button to hide the code editor.
  </Step>
</Steps>

## Bind the UI to the back-end code

You've built the UI and written the code behind it. Let's connect the two! Go back to the browser window with the project editor and do the following:

<Steps>
  <Step title="Observe that the heading at the top of the app is now 'CHATBOT ASSISTANT'">
    Earlier, you saw that the **Header** component's **Text** property was set to `@{my_app.title}`, a value in the app's `state` variable. You changed this value when you update the call to the Writer Framework's `init_state()` method.
  </Step>

  <Step title="Bind the Chatbot component to the 'state' variable's 'conversation' key">
    Recall that the `conversation` object contained within the `state` variable contains the record of the conversation that the user is having with the LLM. Binding the **Chatbot** component to this object allows it to display the conversation to the user.

    Select the **Chatbot** component. In the **properties** panel, find the **Conversation** property and set its value to `@{conversation}`.

    <img alt="Updating the Chatbot's conversation property" />

    The value `@{conversation}` specifies that the **Chatbot** component should get its information from the value corresponding to the `conversation` key in the application's `state` variable.
  </Step>

  <Step title="Specify the Chatbot component's event handler">
    You need to specify that the **Chatbot** component should call the `generate_completion()` function when the user enters a prompt.

    Do this by scrolling down the **properties** panel to the **Events** section until you see a property called **`wf_chatbot_message`**. Select **`generate_completion`** from its menu.

    <img alt="Updating the Chatbot's wf_chatbot_message property" />
  </Step>
</Steps>

## Test the application

You've completed all the steps to make a working chat assistant, and you can try using it right now, even while editing the user interface!

Try entering some prompts into the text entry at the bottom of the **Chatbot** component. The LLM should respond accordingly:

<img alt="The chat assistant, with the project editor in &#x22;UI&#x22; mode" />

To get a better sense of what the experience will be like for the user, switch to the preview by changing the edit mode (located near the upper left corner of the page) from *UI* mode to *Preview* mode by selecting the **Preview** option:

<img alt="The project editor with an arrow pointing to the Preview button" />

Here’s what the app looks like in *Preview* mode:

<img alt="The chat assistant, with the project editor in &#x22;Preview&#x22; mode" />

You can see the output of any `print()` functions and error messages by clicking on the **Log** button located near the upper right corner of the page:

<img alt="The chat assistant with an arrow pointing to the Log button" />

Here’s what the app looks like when displaying the log:

<img alt="The working chat assistant, with the log pane displayed" />

It's very helpful to be able to test the application while editing it. As you continue to work with Writer Framework, you'll find yourself alternating between making changes to your application and testing those changes without having to leave the project editor.

## Run the application locally

Once you've tested the application, it's time to run it locally.

Switch back to your terminal application. Stop the project editor with ctrl-c, then run the application by entering the following command:

```
writer run chat-assistant
```

Note that the command starts with `writer run` as opposed to `writer edit`. This launches the application as your users will see it, without any of the editing tools. Even though you can preview your applications in the project editor, it's still a good idea to test it by running it on your computer, outside the project editor, before deploying it.

You'll be able to access the application with your browser at the URL that appears on the command line. It should look like this:

<img alt="Finished chat assistant project" />

<Note>The Writer editor, which you launched with `writer edit chat-assistant`, and your application, which you launched with `writer run chat-assistant`, run  on the same URL, but on different *ports* (specified by the number after the `:` character at the end of the URL).</Note>

## Conclusion

That's it — you've built a functional chat assistant using the Writer Framework!

Feel free to modify this project! The Writer platform is flexible enough for you to customize, extend, and evolve your application into something completely different! To find out what else you can do, check out the documentation for [Writer Framework](https://dev.writer.com/framework/introduction) and the [Writer API](https://dev.writer.com/introduction).


# Deploy to Writer Cloud
Source: https://dev.writer.com/framework/cloud-deploy



## Basic usage

The `writer cloud` command group includes the following commands:

* `deploy`
* `undeploy`
* `logs`

## Common options

These options are common across multiple commands:

* `--api-key`: The Writer API key used for authentication. If not provided, you will be prompted to enter it.
* `--verbose, -v`: Enable verbose output.

## Commands

<AccordionGroup>
  <Accordion title="writer cloud deploy">
    Deploys an app from the specified path to the Writer cloud.

    **Usage:**

    ```
    writer cloud deploy [OPTIONS] PATH
    ```

    **Arguments:**

    * `PATH`: The path to the folder containing the app to deploy.

    **Options:**

    * `--api-key`: Writer API key for authentication. If not provided, you will be prompted to enter it.
    * `--env, -e`: Environment variables to set in the deployed app. Use the format `VAR=value`. Multiple environment variables can be specified by repeating the `--env` option.
    * `--verbose, -v`: Enable verbose output.

    **Example:**

    ```
    writer cloud deploy hello --env VAR1=value1 --env VAR2=value2
    ```

    **Description:**

    * Deploys the app located in the `PATH` folder.
    * Creates a deployment package, ignoring `.git` directories, `Dockerfile`s and all files specified in `.gitignore` file.
    * Uploads the package to the deployment server.
    * The deployed app will have access to specified environment variables.
    * By default, the `WRITER_API_KEY` environment variable will be added to enable AI features.

    **Output Example**

    ```
    Creating deployment package from path: /path/to/your/app
    [WARNING] Dockerfile found in project root. This will be ignored in the deployment package.
    Packing file: pyproject.toml
    Packing file: README.md
    ...
    Uploading package to deployment server
    Package uploaded. Building...
    ...
    Deployment successful
    URL: https://your_app_url
    ```
  </Accordion>

  <Accordion title="writer cloud undeploy">
    Stops the deployed app and makes it unavailable.

    **Usage:**

    ```
    writer cloud undeploy [OPTIONS]
    ```

    **Options:**

    * `--api-key`: Writer API key for authentication. If not provided, you will be prompted to enter it.
    * `--verbose, -v`: Enable verbose output.

    **Example:**

    ```
    writer cloud undeploy
    ```

    **Description:**

    * Stops and removes the deployed app from the Writer cloud.
  </Accordion>

  <Accordion title="writer cloud logs">
    Fetches logs from the deployed app.

    **Usage:**

    ```
    writer cloud logs [OPTIONS]
    ```

    **Options:**

    * `--api-key`: Writer API key for authentication. If not provided, you will be prompted to enter it.
    * `--verbose, -v`: Enable verbose output.

    **Example:**

    ```
    writer cloud logs
    ```

    **Description:**

    * Continuously fetches and displays logs from the deployed app.
    * Logs are ordered by date and time.

    **Output Example**

    ```
    2024-06-11 09:27:02.190646+00:00 [INFO] Starting container entrypoint...
    2024-06-11 09:27:03.798148+00:00 [INFO] BuildService - Downloading build files...
    ...
    ```
  </Accordion>
</AccordionGroup>

## Environment variables

When deploying an app, you can specify environment variables that will be available to the app during runtime. Use the `--env` option to pass these variables.

```
writer cloud deploy hello --env DB_HOST=db.example.com --env DB_PORT=5432
```

In this example, `DB_HOST` and `DB_PORT` will be available to the app as environment variables.

## API key

The `WRITER_API_KEY` is crucial for deploying and managing apps. It is used for authenticating requests to the Writer cloud. If not provided as an option, the CLI will prompt you to enter it. The `WRITER_API_KEY` will also be added to the deployed app's environment to enable AI features of the Writer framework.

## Deployment process

<Steps>
  <Step title="Package Creation">
    The CLI packages the app, excluding certain files (e.g., Dockerfile, `service_entrypoint.py`).
  </Step>

  <Step title="Upload Package">
    The package is uploaded to the Writer deployment server.
  </Step>

  <Step title="Build and Deploy">
    The server builds and deploys the app, making it accessible via a URL.
  </Step>

  <Step title="Environment Variables">
    Specified environment variables are set, and `WRITER_API_KEY` is added by default.
  </Step>
</Steps>

## Example workflow

<Steps>
  <Step title="Deploying an App">
    ```bash theme={null}
    writer cloud deploy /path/to/app --env DB_HOST=db.example.com --env DB_PORT=5432
    ```
  </Step>

  <Step title="Fetching Logs">
    ```bash theme={null}
    writer cloud logs
    ```
  </Step>

  <Step title="Undeploying an App">
    ```bash theme={null}
    writer cloud undeploy
    ```
  </Step>
</Steps>

By following this documentation, you should be able to effectively deploy and manage your Writer apps using the Writer Framework CLI.


# Components
Source: https://dev.writer.com/framework/component-list-link





# Custom components
Source: https://dev.writer.com/framework/custom-components



It's possible to extend Framework with custom component templates.

They're developed using Vue 3 and TypeScript. Once transpiled, they can be used by copying them to the `extensions/` folder of any project.

<Note>
  Custom components behave exactly like built-in ones. They are just as
  performant, can contain other components, and offer the same the Builder
  experience. They only differ from built-in components in the way that they're
  bundled and imported.
</Note>

## Architecture

Framework front-end compiles to a collection of static assets that is distributed in the Python package. These static assets are then served via FastAPI.

During initialisation time, the server scans the `extensions/` folder in the project folder and looks for `.css` and `.js` files. This folder is also served, similarly to `static/`. If it finds any valid files in `extensions/`, it shares the list with clients and tells them to dynamically import these files during runtime.

Extensions and custom templates are currently synonyms, but this might change in order to accommodate other extension capabilities.

<img alt="Custom Components - Architecture" />

Dependencies are [provided](https://vuejs.org/api/composition-api-dependency-injection.html) using injection symbols and can be *injected* to be used by the component template. These include `evaluatedFields`, which contain the current values of the editable fields. Injected dependencies are fully typed, making development easier.

[Rollup's external feature](https://rollupjs.org/configuration-options/#external), invoked via Vite, allows for extensions to be compiled without dependencies and link those during runtime. Therefore, extensions aren't bundled to be standalone, but rather to work as a piece of a puzzle.

<img alt="Custom Components - External" />

## Anatomy of a template

A template defines how a certain component is rendered. For example, `corebutton` defines how *Button* components are rendered.

Framework component templates are purely front-end. They are Vue 3 templates that extend the Vue specification via a [custom option](https://vuejs.org/api/utility-types.html#componentcustomoptions), `writer`. This custom option defines all the Framework-specific behaviour of the component. For example, its `fields` property establishes which fields will be editable via the Builder.

### Simple example

This example shows a template for *Bubble Message*, a simple demo component with one editable field, `text`.

```js theme={null}
<template>
	<div class="BubbleMessage">
        <div class="triangle"></div>
        <div class="message">

            <!-- Shows the current value of the field "text" -->

            {{ fields.text.value }}
        </div>
	</div>
</template>

<script lang="ts">
export default {
    writer: {
		name: "Bubble Message",
		description: "Shows a message in the shape of a speech bubble.",
		category: "Content",

        // Fields will be editable via Framework Builder

        fields: {
			text: {
				name: "Text",
				type: FieldType.Text,
			},
		},

        // Preview field is used in the Component Tree

		previewField: "text",
	},
};
</script>
<script setup lang="ts">
import { FieldType } from "@/writerTypes";
import injectionKeys from "../injectionKeys";
import { inject } from "vue";

/*
The values for the fields defined earlier in the custom option
will be available using the evaluatedFields injection symbol.
*/

const fields = inject(injectionKeys.evaluatedFields);
</script>

<style scoped>
/* ... */
</style>
```

The code above will make Bubble Message available in the Builder.

<img alt="Custom Components - Bubble Message" />

## Developing templates

### Run a local server

<Steps>
  <Step title="Clone the Framework Repository">
    To get started, clone the [Framework
    repository](https://github.com/writer/writer-framework) from GitHub.
  </Step>

  <Step title="Set Up the Development Environment">
    To develop custom templates in a developer-friendly way, ensure you have a
    front-end development server with instant reload capabilities. The front-end
    code for Framework is located in the `ui` folder. With Node and npm
    installed on your system, run `npm install` to install dependencies. Then,
    start the server with support for custom component templates using `npm run
            custom.dev`.

    ```sh cd ui npm install # "custom.dev" links templates in theme={null}
    "custom_components/" # "dev" runs the server without them npm run custom.dev
    ```
  </Step>

  <Step title="Start the Back-End Server">
    The command `npm run custom.dev` starts a front-end server, which requires a
    back-end to function fully. Start Framework via command line, specifying the
    option `--port 5000`, to provide a back-end on that port. It's recommended
    to create a new app for testing the template you're developing. `sh writer
            create customtester writer edit customtester --port 5000 `
  </Step>

  <Step title="Access Framework and Test Custom Component">
    You should now be able to access Framework via the URL provided by Vite,
    e.g. `http://localhost:5174`. In the Builder's *Toolkit*, you should see the
    sample component, *Balloon Message*. Add it to your tester application.
  </Step>
</Steps>

### Create a new component

<Tip>
  You can also have a look at the built-in component templates, since their
  syntax is equivalent. They can be found in the `ui/src/components/core` folder.
</Tip>

Go to `ui/src/components/custom` and open the Vue single-file components, i.e. the
`.vue` files. These files contain comments that will help you get started. Try editing
the provided templates, you should see changes reflected.

You can get started by duplicating one of these examples. Make sure you add the new template to the entrypoint, as discussed below.

### Define entrypoint

For custom component templates to be taken into account, they need to be accessible from the entrypoint. Edit `ui/src/components/custom/index.ts` to define which templates you wish to export and under which identifiers.

```ts theme={null}
// Import the templates

import BubbleMessage from "./BubbleMessage.vue";
import BubbleMessageAdvanced from "./BubbleMessageAdvanced.vue";

// Export an object with the ids and the templates as default

export default {
  bubblemessage: BubbleMessage,
  bubblemessageadvanced: BubbleMessageAdvanced,
};
```

A single or multiple templates can be specified. Take into account that they will all be exported, and later imported, together.

## Bundling templates

Execute `npm run custom.build` into `src/ui`, this will generate the output `.js` and `.css` files into `./custom_components_dist`.

```sh theme={null}
# "build" builds the entire front-end
# "custom.build" only builds the custom templates

npm run custom.check # Optional: checks certain issues on custom components
npm run custom.build
```

Collect the files from `./custom_components_dist` and pack them in a folder such as `my_custom_bubbles`. The folder containing the generated files, e.g. `my_custom_bubbles`, can now be placed in the `extensions/` folder of any Framework project. It'll be automatically detected during server startup.

<Tip>
  The `custom.check` command is optional, but it's recommended to run it before building the custom components. It checks for common issues in the custom components, such as invalid key declaration, ...
</Tip>


# Custom server
Source: https://dev.writer.com/framework/custom-server



Framework uses Uvicorn and serves the app in the root path i.e. `/`. If you need to use another ASGI-compatible server or fine-tune Uvicorn, you can easily do so.

## Configure webserver

You can tune your server by adding a `server_setup.py` file to the root
of your application, next to the `main.py` files.

This file is executed before starting writer. It allows you to configure [authentication](./authentication.md),
add your own routes and middlewares on FastAPI.

```python theme={null}
# server_setup.py
import typing

import writer.serve

if typing.TYPE_CHECKING:
    from fastapi import FastAPI

# Returns the FastAPI application associated with the writer server.
asgi_app: FastAPI = writer.serve.app

@asgi_app.get("/probes/healthcheck")
def hello():
    return "1"
```

<Warning>
  `server_setup.py` is disabled by default on edit mode. If you want to use in `edit` mode, you can launch `writer edit --enable-server-setup <app>`.
</Warning>

## Implement custom server

You can import `writer.serve` and use the function `get_asgi_app`. This returns an ASGI app created by FastAPI, which you can choose how to serve.

The following code can serve as a starting point. You can save this code as `serve.py` and run it with `python serve.py`.

```py theme={null}
import uvicorn
import writer.serve

app_path = "." # . for current working directory
mode = "run" # run or edit

asgi_app = writer.serve.get_asgi_app(app_path, mode)

uvicorn.run(asgi_app,
    host="0.0.0.0",
    port=5328,
    log_level="warning",
    ws_max_size=writer.serve.MAX_WEBSOCKET_MESSAGE_SIZE)
```

Note the inclusion of the imported `ws_max_size` setting. This is important for normal functioning of the framework when dealing with bigger files.

Fine-tuning Uvicorn allows you to set up SSL, configure proxy headers, etc, which can prove vital in complex deployments.

If you want to disable server setup hook, you should use `enable_server_setup`:

```python theme={null}
asgi_app = writer.serve.get_asgi_app(app_path, mode, enable_server_setup=False)
```

## Multiple apps at once

Framework is built using relative paths, so it can be served from any path. This allows multiple apps to be simultaneously served on different paths.

The example below uses the `get_asgi_app` function to obtain two separate Framework apps, which are then mounted on different paths, `/app1` and `/app2`, of a FastAPI app.

```py theme={null}
import uvicorn
import writer.serve
from fastapi import FastAPI, Response

root_asgi_app = FastAPI(lifespan=writer.serve.lifespan)
sub_asgi_app_1 = writer.serve.get_asgi_app("../app1", "run")
sub_asgi_app_2 = writer.serve.get_asgi_app("../app2", "run")

root_asgi_app.mount("/app1", sub_asgi_app_1)
root_asgi_app.mount("/app2", sub_asgi_app_2)

@root_asgi_app.get("/")
async def init():
    return Response("""
    <h1>Welcome to the App Hub</h1>
    """)

uvicorn.run(root_asgi_app,
    host="0.0.0.0",
    port=5328,
    log_level="warning",
    ws_max_size=writer.serve.MAX_WEBSOCKET_MESSAGE_SIZE)
```


# Dataframe
Source: https://dev.writer.com/framework/dataframe



## DataFrames

If your application needs to present data as a table, it should use a **DataFrame**. DataFrames provide a simple way to present data in a grid format, require only a couple of lines of code to set up, and provide an interface that users expect from modern data applications.

<img alt="DataFrame showing a table of popular ice cream flavors" />

DataFrames built-in features that users expect, such as headers that can be clicked to change the sort order and resizable columns...

<img alt="DataFrame with arrows pointing to the re-sort and resizing features" />

...and with the simple change of a parameter, you can enable features such as the Search field, which lets the user find the data they’re looking for (or filter out unwanted data), and the Download button, which downloads the data currently being displayed as a .csv file:

<img alt="DataFrame with arrows pointing to the Search field and Download button" />

You can find the full list of DataFrame properties and fields on the [*DataFrame* component page](/components/dataframe).

### “DataFrame” has multiple meanings

**Writer Framework has two objects with the name *DataFrame*:**

1. **UI DataFrame:** In Writer Framework's UI, "DataFrame" refers to a ***user interface component*** that displays data in rows and columns in a way similar to a spreadsheet or SQL table.
2. **Code Dataframe:** In code that you write for a Writer Framework application, `DataFrame` refers to a ***data structure*** that stores data in rows and columns in a way similar to a spreadsheet or SQL table.

To present a data table in Writer Framework, you create a `DataFrame` data structure in your code and then bind it to a UI Dataframe.

## Displaying a static DataFrame

A static DataFrame is one whose content does not change. The user can change its sort order, but the data within the DataFrame remains constant.

<Steps>
  <Step title="Create a DataFrame data structure">
    Writer Framework supports both [pandas](https://pandas.pydata.org/) and [Polars](https://pola.rs/) `DataFrame` data structures. Create a `DataFrame`, assign its value to a variable, then assign make that variable a value in the `state` dictionary:

    <CodeGroup>
      ```python pandas theme={null}
      import writer as wf
      import pandas as pd

      data = [
      	{"rank": 1, "flavor": "Vanilla", "favorite": 0.11},
      	{"rank": 2, "flavor": "Chocolate", "favorite": 0.1},
      	{"rank": 3, "flavor": "Cookies and cream", "favorite": 0.07},
      	{"rank": 4, "flavor": "Strawberry", "favorite": 0.06},
      	{"rank": 5, "flavor": "Chocolate chip", "favorite": 0.02},
      ]
      df = pd.DataFrame(data)

      wf.init_state({
      	"mydf": df
      })
      ```

      ```python Polars theme={null}
      import writer as wf
      import polars as pl

      data = [
      	{"rank": 1, "flavor": "Vanilla", "favorite": 0.11},
      	{"rank": 2, "flavor": "Chocolate", "favorite": 0.1},
      	{"rank": 3, "flavor": "Cookies and cream", "favorite": 0.07},
      	{"rank": 4, "flavor": "Strawberry", "favorite": 0.06},
      	{"rank": 5, "flavor": "Chocolate chip", "favorite": 0.02},
      ]
      df = pl.DataFrame(data)

      wf.init_state({
      	"mydf": df
      })
      ```
    </CodeGroup>

    The call to `wf.init_state()` adds the `DataFrame` to the application's `state` variable as the value of the `mydf` key.
  </Step>

  <Step title="Add a DataFrame component to the UI and bind it to the DataFrame data structure">
    Add a DataFrame UI component to the user interface, then set its **Data** property to `@{`*dataframe\_key*`}`, where *dataframe\_key* is the `state` variable key whose value refers to the `DataFrame` data structure.

    In the case of this example, `mydf` is the `state` variable key referring to the `DataFrame`, so set the **Data** property to `@{mydf}`.

    <img alt="DataFrame for static table example with properties panel open" />
  </Step>
</Steps>

## Displaying an editable DataFrame

A editable DataFrame is one whose content can change. Like static DataFrames, editable DataFrames use the **DataFrame** UI component. Unlike static tables, the DataFrame UI component is bound to an instance of `EditableDataFrame`, a class provided by the Writer library. Changes to a  `EditableDataFrame` object will be immediately reflected in the DataFrame UI component that it is bound to.

<Steps>
  <Step title="Create an EditableDataFrame data structure">
    An `EditableDataFrame` object can be instantiated from any of the following:

    1. A pandas `DataFrame`
    2. A Polars `DataFrame`
    3. A list of dictionaries

    <CodeGroup>
      ```python pandas theme={null}
      import writer as wf
      import pandas as pd

      data = [
      	{"rank": 1, "flavor": "Vanilla", "favorite": 0.11},
      	{"rank": 2, "flavor": "Chocolate", "favorite": 0.1},
      	{"rank": 3, "flavor": "Cookies and cream", "favorite": 0.07},
      	{"rank": 4, "flavor": "Strawberry", "favorite": 0.06},
      	{"rank": 5, "flavor": "Chocolate chip", "favorite": 0.02},
      ]
      df = pd.DataFrame(data)

      wf.init_state({
      	"mydf": wf.EditableDataFrame(df)
      })
      ```

      ```python Polars theme={null}
      import writer as wf
      import polars as pl

      data = [
      	{"rank": 1, "flavor": "Vanilla", "favorite": 0.11},
      	{"rank": 2, "flavor": "Chocolate", "favorite": 0.1},
      	{"rank": 3, "flavor": "Cookies and cream", "favorite": 0.07},
      	{"rank": 4, "flavor": "Strawberry", "favorite": 0.06},
      	{"rank": 5, "flavor": "Chocolate chip", "favorite": 0.02},
      ]
      df = pl.DataFrame(data)

      wf.init_state({
      	"mydf": wf.EditableDataFrame(df)
      })
      ```

      ```python List of dictionaries theme={null}
      import writer as wf

      data = [
      	{"rank": 1, "flavor": "Vanilla", "favorite": 0.11},
      	{"rank": 2, "flavor": "Chocolate", "favorite": 0.1},
      	{"rank": 3, "flavor": "Cookies and cream", "favorite": 0.07},
      	{"rank": 4, "flavor": "Strawberry", "favorite": 0.06},
      	{"rank": 5, "flavor": "Chocolate chip", "favorite": 0.02},
      ]

      wf.init_state({
      	"mydf": wf.EditableDataFrame(data)
      })
      ```
    </CodeGroup>

    The call to `wf.init_state()` adds the `DataFrame` to the application's `state` variable as the value of the `mydf` key.
  </Step>

  <Step title="Add a DataFrame component to the UI and bind it to the DataFrame data structure">
    Add a **DataFrame** component to the user interface, then set its **Data** property to `@{`*dataframe\_key*`}`, where *dataframe\_key* is the `state` variable key whose value refers to the `DataFrame` data structure.

    In the case of this example, `mydf` is the `state` variable key referring to the `DataFrame`, so set the **Data** property to `@{mydf}`.

    <img alt="DataFrame for dynamic table example with properties panel open" />
  </Step>
</Steps>

## Updating an editable DataFrame

Editable DataFrames are updated by updating the `EditableDataFrame` object they are bound to, which is done using `EditableDataFrame`'s methods.

### `record_add`: Add a new row

`record_add()` adds a new row to an `EditableDataFrame`. It takes a dictionary with the following structure...

```python theme={null}
{"record": new_row}
```

...where `new_row` is a dictionary containing the data for the row to be added.

In the code example above, you would add a new row to the DataFrame with the following code:

```python theme={null}
state["mydf"].record_add({"record": {"rank": 6, "flavor": "Birthday cake", "favorite": 0.01}})
```

### `record`: Read the contents of a row

`record()` returns a row in an `EditableDataFrame`. It takes an integer specifying the index of the row.

In the code example above, you would retrieve the record at row 1 with the following code:

```python theme={null}
record = state["mydf"].record(1)
```

### `record_update`: Change an existing row

`record_update()` replaces an existing row in an `EditableDataFrame` with a new one. It takes a dictionary with the following structure...

```python theme={null}
{
	"record_index": index,
	"record": row_to_update
}
```

...where `index` is an integer specifying which row should be updated and `row_to_update` is a dictionary containing the updated row data.

In the code example above, you would update the row at index 0 with the following code:

```python theme={null}
state["mydf"].record_update({
	"record_index": 0,
	"record": {"rank": 6, "flavor": "Bubble gum", "favorite": 0.08}
})
```

### `record_remove`: Delete an existing row

`record_remove()` removes an existing row from an `EditableDataFrame`. It takes a dictionary with the following structure...

```python theme={null}
{"record_index": index}
```

...where `index` is an integer specifying which row should be deleted.

In the code example above, you would delete the row at index 2 with the following code:

```python theme={null}
state["mydf"].record_remove({"record_index": 2})
```

## Enabling additional features

The DataFrame component has these “always-on” features:

1. **Sorting:** Clicking a column header sorts the entire DataFrame based on that column's values. The first click sorts that DataFrame in ascending order, a second click changes it to descending order, and a third click restores the DataFrame to its original sort order.
2. **Column resizing:** Click and drag the dividing line on the right edge of a column header to adjust its width.

DataFrames have other features that you need to activate, which are listed below.

### Search field

To enable the Search field, select the DataFrame, open the Component settings panel, and set **Enable search** to **yes**.

<img alt="Enabling the Search field in a DataFrame" />

### Download button

To enable the Download button, select the DataFrame, open the Component settings panel, and set **Enable download** to **yes**.

<img alt="Enabling the Download button in a DataFrame" />


# Deploy with Docker
Source: https://dev.writer.com/framework/deploy-with-docker



<Tip>
  To deploy on the Writer cloud see instructions [here](/framework/quickstart#deploying-on-writer-cloud).
</Tip>

You can use Docker to deploy Framework anywhere. If you're an experienced Docker user, you may want to go straight to the provided Dockerfile.

## Creating a Docker image

<Steps>
  <Step title="Verify docker installation">
    Make sure you have Docker installed on your system.
  </Step>

  <Step title="Navigate to your app's folder">
    Open a terminal and navigate to your app’s folder.
  </Step>

  <Step title="Set up Python environment">
    Create a `pyproject.toml` file using `poetry init` and install `writer` using `poetry add writer`.
  </Step>
</Steps>

A Dockerfile is a file with instructions that tell Docker how to build your image. It must be named `Dockerfile`. You can use the following as-is, or as a starting point. It should be saved in your app's folder, together with `main.py` and `.wf/`.

```docker theme={null}
FROM python:3.10-bullseye
RUN apt-get update -y && mkdir /app
RUN apt-get install build-essential cmake python3-dev -y
COPY . /app
WORKDIR /app
RUN pip3 install poetry
RUN poetry config virtualenvs.create false
RUN poetry install --only main
ENTRYPOINT [ "writer", "run" ]
EXPOSE 8080
CMD [ ".",  "--port", "8080", "--host", "0.0.0.0" ] 
```

<Note>
  This Dockerfile is just a guideline.
  It uses an official Python slim base image with a multistage build to reduce the size of the built image.
  If you're a Docker expert, feel free to work on your own `Dockerfile`. Framework is, after all, a standard Python package.
</Note>

To build the image, use `docker build . -t ` followed by an image tag, which you're free to choose and will locally identify your image.

```sh theme={null}
docker build . -t my_framework_app
```

<Warning>
  By default, Docker builds images in the architecture it's being run on. If you're working with an ARM computer, such as a Mac M2 or a Raspberry Pi, Docker will build an ARM image. Most cloud services will only accept x86 images. You can use another computer (or virtual machine) to build the image, or you can use [Docker buildx](https://docs.docker.com/build/building/multi-platform/).
</Warning>

## Publishing your Docker image

Once your Docker image has been built, you can publish it to a registry. This is a place where Docker images are stored and made available to other services.

We recommend using Docker Hub; it has a generous free tier, it's very easy to set up and it's widely supported. However, you can choose to use another service such as Azure Container Registry. To use Docker Hub, you'll need to sign up for an account.

You can push your image using the following commands.

```sh theme={null}
# Login to Docker Hub
docker login

# Push the image
# Replace "my_writer_app" for the tag you've previously chosen
# Replace "my_user" for your user on Docker Hub
docker tag my_writer_app:latest my_user/my_writer_app:latest
docker push my_user/my_writer_app:latest
```

If your image is public, anyone can now run the following command and start the app. It's important to bind the port to a port in the host machine. The command below binds port 8080 in the Docker image to port 8080 in the host.

```sh theme={null}
docker run -p 8080:8080 my_user/my_framework_app
```

Go on your browser to [http://localhost:8080](http://localhost:8080) to check everything is working as expected.

## Deploying your Docker image

As mentioned earlier, once the image is a registry, it can be spun up by others. After trying a few options, we recommend using Google Cloud Run. Its free tier is generous and SSL works out of the box.

<img alt="Run and Share - Google Cloud Run" />

Cloud Run can be configured in just one page. It takes the image from a registry and makes it available via a URL, with SSL enabled by default. We recommend the following settings:

<AccordionGroup>
  <Accordion title="Instance specifications">
    Minimum 0 instances, maximum 4 instances. This range is suitable unless your app needs to serve several thousands of users.
  </Accordion>

  <Accordion title="Connection settings">
    Set the request timeout to the maximum allowed and enable Session Affinity. This ensures that WebSocket connections are not unnecessarily dropped.
  </Accordion>

  <Accordion title="Resource allocation">
    Allocate 2GB of memory and 2 vCPUs. This configuration will likely be enough to comfortably run a simple app. However, you can probably manage with much less—512MB of memory and 1 vCPU—if your app isn’t too demanding and you don’t expect much traffic.
  </Accordion>
</AccordionGroup>


# Event handlers
Source: https://dev.writer.com/framework/event-handlers



Events originate in the front-end, for example, when a user clicks a *Button* component. Using the Builder, these events can be linked to event handlers.

## Plain Python functions

Event handlers are Python functions accessible from `main.py`. They can be defined in that same file or imported. No decorators or special syntax are required.

```py theme={null}
# This event handler will add an entry to the log
def handle_click()
    print("Hello")
```

To specify that a function isn't an event handler and should remain hidden to the front-end, prefix it with a `_` (underscore).

```py theme={null}
# This function won't be visible in the front-end
# because its name starts with an underscore
def _reticulate(splines):
    r_splines = np.random.normal(size=(splines,100))
    return r_splines
```

### External handlers

If your `main.py` file has become cluttered with too many handler functions, you can organize them more effectively using the `init_handlers` method. This method allows you to register handler functions from other modules. You can pass a single imported module or a list of modules to the init\_handlers method to register multiple handlers simultaneously:

<CodeGroup>
  ```py one_handler theme={null}
  # my_app/my_handlers_module.py

  def increment(state):
      state["counter"] += 1
  ```

  ```py init_handlers one module theme={null}
  # my_app/main.py

  import writer as wf
  import my_handlers_module

  wf.init_handlers(my_handlers_module)
  # Register all functions from the module as handlers;
  # this makes `increment` handler accessible on front-end
  ```

  ```py init_handlers many modules theme={null}
  # my_app/main.py

  import writer as wf
  import handler_module_one
  import handler_module_two

  wf.init_handlers([handler_module_one, handler_module_two])
  ```
</CodeGroup>

<Warning>
  Each function inside a module is attempted to be registered as a handler. Make sure to use `_` prefix as described [before](#plain-python-functions) to prevent exposing unwanted functions to front-end.
</Warning>

You can also call `init_handlers` within other modules, which allows for a sequence of registrations:

<CodeGroup>
  ```py another_handlers_module theme={null}
  # my_app/another_handlers_module.py

  def decrement(state):
      state["counter"] -= 1
  ```

  ```py register_additional_handlers theme={null}
  # my_app/my_handlers_module.py

  import writer as wf
  import another_handlers_module

  wf.init_handlers(another_handlers_module)
  # Makes `decrement` handler accessible on front-end

  ...
  ```

  ```py my_handlers_module theme={null}
  # my_app/main.py

  import writer as wf
  import my_handlers_module

  ...
  ```
</CodeGroup>

Note that for this "chain" to work, you need to import the final module in the sequence into `main.py`.

## Mutating state

In most cases, event handlers will modify the application state. State can be accessed by including the `state` argument in the handler, which will provide you with a `WriterState` object for the session that invoked the handler.

Elements of state can be reached using the square brackets syntax `state["my_element"]`. Accessing keys that don't exist will return `None`.

```py theme={null}
def handle_click(state):
    state["counter"] += 1
```

The handler above receives the application state for the relevant session and mutates it. For example, if Bob's counter was 4, and he clicks on a *Button* linked to `handle_click`, his new counter value will be 5. Other sessions remain unaffected.

## Mutation detection

<Warning>
  Mutations are detected via assignment.
  Make sure you perform an assignment on the state element you're mutating, for the mutation to be detected.
</Warning>

When communicating with the front-end, Framework only sends state elements that have mutated.

To detect which elements have mutated, it relies on assignment (via operators such as `=`, `+=`, etc). This is because Python doesn't offer a performant, reliable mechanism to detect mutations. See the two examples below.

<CodeGroup>
  ```python hande_click theme={null}
  def handle_click(state):
      state["my_df"].sample(frac=1, random_state=random.seed())

      # The self-assignment is necessary when mutating
      # an existing object directly on state

      state["my_df"] = state["my_df"]
  ```

  ```python hande_click_cleaner theme={null}
  # The following cleaner code also works as it relies on assignment
  def hande_click_cleaner(state):
      my_df = state["my_df"]
      my_df.sample(frac=1, random_state=random.seed())
      state["my_df"] = my_df # State assignmnet
  ```
</CodeGroup>

## Mutation event

You can subscribe to mutations on a specific key in the state.
This is useful when you want to trigger a function every time a specific key is mutated.

<CodeGroup>
  ```python simple subscription theme={null}
  import writer as wf

  def _increment_counter(state):
      state['my_counter'] += 1

  state = wf.init_state({"a": 1, "my_counter": 0})
  state.subscribe_mutation('a', _increment_counter)

  state['a'] = 2  # trigger _increment_counter mutation
  ```

  ```python multiple subscriptions theme={null}
  import writer as wf

  def _increment_counter(state):
      state['my_counter'] += 1

  state = wf.init_state({
  	'title': 'Hello',
  	'app': {'title', 'Writer Framework'},
  	'my_counter': 0}
  )

  state.subscribe_mutation(['title', 'app.title'], _increment_counter)  # subscribe to multiple keys

  state['title'] = "Hello Pigeon"  # trigger _increment_counter mutation
  ```

  ```python trigger event handler theme={null}
  import writer as wf

  def _increment_counter(state, context: dict, payload: dict, session: dict, ui: WriterUIManager):
  	if context['event'] == 'mutation' and context['mutation'] == 'a':
  		if payload['previous_value'] > payload['new_value']:
  			state['my_counter'] += 1

  state = wf.init_state({"a": 1, "my_counter": 0})
  state.subscribe_mutation('a', _increment_counter)

  state['a'] = 2  # increment my_counter
  state['a'] = 3  # increment my_counter
  state['a'] = 2  # do nothing
  ```
</CodeGroup>

<Tip>
  `subscribe_mutation` is compatible with event handler signature. It will accept all the arguments
  of the event handler (`context`, `payload`, ...).
</Tip>

## Receiving a payload

Several events include additional data, known as the event's payload. The event handler can receive that data using the `payload` argument.

For example, the `wf-change` event in a *Text Input* component is triggered every time the value changes. As a payload, it includes the new value.

```py theme={null}
def handle_input_change(state, payload):
    state["value"] = payload
```

The content of the payload will vary depending on the event. For example, when a user takes a photo with a *Webcam Capture*, the picture they took is sent across as a PNG image.

```py theme={null}
def handle_webcam_capture(payload):
	image_file = payload
	with open(f"picture.png", "wb") as file_handle:
		file_handle.write(image_file)
```

Handling different payloads across events can be challenging, especially since the shape of the payload may vary. To simplify this process, the Builder provides stub code that can help you get started with writing an event handler. You can access it by clicking the icon located next to the event when configuring the component's settings. This feature can help you quickly understand the structure of the payload and start writing the appropriate code to handle it.

## Globals

You can use globals and module attributes, just as you would in a standard Python script. This is very convenient for storing a single copy of resource-intensive object.

```py theme={null}
my_ai = CatIdentifierAI()

def evaluate(state, payload):
    result = my_ai.process(payload)
    state["is_a_cat"] = result
```

Take into account that globals apply to all users. If you need to store data that's only relevant to a particular user, use application state.

## Middlewares

Middlewares are functions that run before and after every event handler.
They can be used to perform tasks such as logging, error handling, session management, or modifying the state.

```py theme={null}
import writer as wf

@wf.middleware()
def middleware_before(state, payload, context):
	print("Middleware before event handler")
	state['running'] += 1
	yield
	print("Middleware after event handler")
	state['running'] -= 1
```

A middleware receives the same parameters as an event handler.

A middleware can be used to handle exceptions that happens in event handlers.

```py theme={null}
import writer as wf

@wf.middleware()
def middleware_before(state):
	try:
		yield
	except Exception as e:
		state['error_counter'] += 1
		state['last_error'] = str()
	finally:
		pass
```

## Standard output

The standard output of an app is captured and shown in the code editor's log. You can use the standard `print` function to output results.

```py theme={null}
# Shown every time the app starts
print("Hello world")

def payload_inspector(state, payload):
    # Shown every time the event handler is executed
    print("Payload: " + repr(payload))
```

## Execution flow

Event handlers run in a thread pool and are non-blocking. Each event is processed independently from each other.

State mutations are sent to the front-end after the function has finished executing. The code in `handle_fast` will accumulate all mutations and send to the front-end after the function returns. For long-running tasks, Framework will periodically check state and provide partial updates to the user.

<CodeGroup>
  ```py handle_fast theme={null}
  def handle_fast(state):
      state["text"] = "Hello"
      state["x"] += 3
      state["y"] += 2
  ```

  ```py handle slowly theme={null}
  # The code below will set `message` to "Loading...", then to "Completed".  
  def handle_slowly(state):
      state["message"] = "Loading..."
      import time
      time.sleep(5)
      state["message"] = "Completed"
  ```
</CodeGroup>

## Asynchronous event handlers

Framework supports asynchronous event handlers, allowing for non-blocking I/O operations directly within event handlers. This is particularly useful for tasks such as fetching data from a database, making HTTP requests, or performing any other I/O bound operation that can benefit from asynchronous execution.

### Defining an asynchronous handler

An asynchronous event handler is defined with the standard `async` keyword syntax.

```py theme={null}
# An asynchronous event handler for performing an I/O bound operation
async def handle_async_click(state):
    data = await fetch_data()
    state["data"] = data
```

In the example above, `fetch_data()` is an asynchronous function that retrieves data, potentially from a remote source. The `await` keyword is used to wait for the operation to complete without blocking the main thread, allowing other tasks to run concurrently.

### Awaitable objects

You can use any awaitable object within an async event handler. This includes the output of any function defined with `async def`, or objects with an `__await__` method. This makes it easy to integrate with asynchronous libraries and frameworks.

## Context

The `context` argument provides additional information about the event.

The context provide the id of component that trigger the event in `target` field.

```py theme={null}
def handle_click(state, context: dict):
	last_source_of_click = context['target']
	state["last_source_of_click"] = last_source_of_click
```

The context provides the event triggered in the `event` field.

```py theme={null}
def handle_click(state, context: dict):
	event_type = context['event']
	if event_type == 'click':
		state["last_event"] = 'Click'
```

The repeater components have additional fields in the context, such as defined in `keyVariable` and `valueVariable`.

```py theme={null}
def handle_repeater_click(state, context: dict):
	key = context['keyVariable']
	state['repeater_content'][key]['last_action'] = 'Clicked' 
```

More information in [Repeater chapter](/framework/repeater)


# Frontend scripts
Source: https://dev.writer.com/framework/frontend-scripts



Framework can import custom JavaScript/ES6 modules from the front-end. Module functions can be triggered from the back-end.

## Importing an ES6 module

Similarly to [stylesheets](/framework/stylesheets), front-end scripts are imported via Framework's `mail` capability. This allows you to trigger an import for all or specific sessions at any time during runtime. When the `import_frontend_module` method is called, this triggers a dynamic [import()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) call in the front-end.

The `import_frontend_module` method takes the `module_key` and `specifier` arguments. The `module_key` is an identifier used to store the reference to the module, which will be used later to call the module's functions. The `specifier` is the path to the module, such as `/static/mymodule.js`. It needs to be available to the front-end, so storing in the `/static/` folder is recommended.

The following code imports a module during event handling.

```py theme={null}
def handle_click(state):
    state.import_frontend_module("my_script", "/static/mymodule.js")
```

If you want the module to be imported during initialisation, use the initial state.

```py theme={null}
initial_state = wf.init_state({
    "counter": 1
})

initial_state.import_frontend_module("my_script", "/static/mymodule.js")
```

<Note>
  Use versions to avoid caching. Similarly to stylesheets, your browser may cache modules, preventing updates from being reflected. Append a querystring to invalidate the cache, e.g. use `/static/script.js?3`.
</Note>

## Writing a module

The module should be a standard ES6 module and export at least one function, enabling it to be triggered from the back-end. As per JavaScript development best practices, modules should have no side effects. An example of a module is shown below.

```js theme={null}
let i = 0;

export function sendAlert(personName) {
    i++;
    alert(`${personName}, you've been alerted. This is alert ${i}.`);
}
```

## Calling a function

Once the module is imported, functions can be called from the back-end using the `call_frontend_function` method of state. This function takes three arguments. The first, `module_key` is the identifier used to import the module. The second, `function_name` is the name of the exported front-end function. The third, `args` is a `List` containing the arguments for the call.

The following event handler triggers the front-end function defined in the section above.

```py theme={null}
def handle_click(state):
    state.call_frontend_function("mymodule", "sendAlert", ["Bob"])
```

## Import a JS script

Framework can also import and run JavaScript scripts directly, for their side effects. These are imported via the report's `import_script` method. This method takes two arguments. The first, `script_key` is the identifier used to import the script. The second, `path` is the path to the file. The specified path must be available to the front-end, so storing it in your application's `./static` folder is recommended.

```py theme={null}
initial_state = wf.init_state({
    "counter": 1
})

initial_state.import_script("my_script", "/static/script.js")
```

<Warning>
  Prefer ES6 modules: importing scripts is useful to import libraries that don't support ES6 modules. When possible, use ES6 modules. The `import_script` syntax is only used for side effects; you'll only be able to call functions from the back-end using modules that have been previously imported via `import_frontend_module`.
</Warning>

## Importing a script from a URL

Framework can also import scripts and stylesheets from URLs. This is useful for importing libraries from CDNs. The `import_script` and `import_stylesheet` methods take a `url` argument, which is the URL to the script or stylesheet.

```python theme={null}
initial_state = wf.init_state({
    "my_app": {
        "title": "My App"
    },
})

initial_state.import_script("lodash", "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.js")
```

## Frontend core

<Warning>
  Effectively using Framework's core can be challenging and will likely entail reading its [source code](https://github.com/writer/writer-framework/blob/master/src/ui/src/core/index.ts). Furthermore, it's considered an internal capability rather than a public API, so it may unexpectedly change between releases.
</Warning>

You can access Framework's front-end core via `globalThis.core`, unlocking all sorts of functionality. Notably, you can use `getUserState()` to get values from state.

```js theme={null}
export function alertHueRotationValue() {
    const state = globalThis.core.userState.value.
    console.log("State is", state);
}
```


# Handling inputs
Source: https://dev.writer.com/framework/handling-inputs



There are two, complementary, ways to handle inputs in Framework: via event handlers and via binding.

## Event handlers

Input components have *change* events that are dispatched when the value changes. The new value is provided as a payload in the event handler. Change events have slightly different names across components, reflecting the payloads they provide. For example, *Number Input* and *Slider Input* use the event `wf-number-change` while *Text Input* and *Text Area Input* use the generic `wf-change`.

As discussed in the [Event handlers](/framework/event-handlers) section, the payload can be accessed via the `payload` argument in the event handler.

```py theme={null}
# This event handler takes the payload and assigns it
# to the state element "name"
def handle_input_change(state, payload):
    state["name"] = payload
```

## Two-way bindings

Writing event handlers for every input component can be tedious. In most cases, you'll only need to update a single element of state when the value changes, akin to the example above. You can achieve this by binding a component to a state element.

Bindings automatically handle the *change* event for the component and set the value of the state element to the payload. Furthermore, bindings are two-way. If the state element is updated from the back-end, the front-end component is updated to reflect the new value.

As mentioned in the [Builder basics](/framework/builder-basics) section of the guide, bindings can be configured in the component settings.

<img alt="Repeater example" />

The binding above establishes a two-way link between the component and the state element `name`. If `name` changes in the back-end, the component changes. If the component changes, the value of `name` changes.

## Using events and bindings simultaneously

Bindings can be used together with events. This is useful for triggering recalculations or applying dynamic filters. For example, you may want to have three *Number Input* components bound to `a`, `b` and `c` and display a value `n`. This easily done by binding the components and linking the same recalculation event handler to all three components.

```py theme={null}
def recalculate(state):
    state["n"] = state["a"]*state["b"]*state["c"]
```

## Handling inputs safely

Framework automatically sanitises the payloads it provides for its built-in events, those that start with `wf-`.

For example, if a *Dropdown Input* component lists options `high` and `low`, you're guaranteed you won't get a value like `"Robert'); DROP TABLE students;--"` when handling `wf-option-change`. You'll get `"high"`, `"low"` or `None`.

<Warning>
  Inputs are sanitised, but you should still be careful

  As with any application, it's important to be familiar with the risks associated with handling user input, especially SQL injections. If you're using any custom HTML and mixing it with user generated content, make sure you understand XSS.
</Warning>

## Creating forms

Input components can be combined with *Message* and *Button* components to create forms with messages, indicating whether the submission was successful.

<img alt="Form example" />


# Writer Framework
Source: https://dev.writer.com/framework/introduction



The Writer Framework lets you build feature-rich apps by using a drag-and-drop visual editor called **the Builder** and writing the back-end code in Python. It's fast and flexible, with clean, easy-to-test syntax. It provides separation of concerns between UI and business logic, enabling more complex apps.

<img alt="Framework Builder screenshot" />

Build AI apps with the Writer Framework when:

1. You need to incorporate external data sources, such as external APIs
2. You have complex user input that requires custom logic, such as conditions that trigger the use of different prompts
3. You want to quickly analyze and visualize data using an LLM

The Writer Framework offers:

<AccordionGroup>
  <Accordion title="Easily-testable Python functions">
    Define event handlers as plain Python functions.

    ```python theme={null}
    def handle_text_update(state):
        state["text"] = "Updated text"

    writer.init_state({
        "text": "Initial text"
    })
    ```
  </Accordion>

  <Accordion title="Visual editor">
    Link the event handler and state to the UI seamlessly.
  </Accordion>

  <Accordion title="Standard Python packages">
    Install with a simple pip command.
  </Accordion>

  <Accordion title="Version control">
    Save user interfaces as JSON to be version controlled with the rest of the app.
  </Accordion>

  <Accordion title="Flexible editing">
    Use your local code editor with instant refreshes or the provided web-based editor. Edit the UI while your app is running without needing to click “Preview.”
  </Accordion>

  <Accordion title="Minimal overhead">
    Event handling adds only 1-2ms to your Python code.
  </Accordion>

  <Accordion title="Real-time synchronization">
    Use WebSockets to synchronize front-end and back-end states.
  </Accordion>

  <Accordion title="Efficient execution">
    Non-blocking by default, with asynchronous event handling in a dedicated thread pool.
  </Accordion>

  <Accordion title="Customizable elements">
    No CSS required for customization like shadows, button icons, and background colors.
  </Accordion>

  <Accordion title="HTML integration">
    Include HTML elements with custom CSS using the HTML Element component, which can serve as containers for built-in components.
  </Accordion>
</AccordionGroup>

To get started, head to [Quickstart](/framework/quickstart) or our tutorials:

<CardGroup>
  <Card title="Social post generator" icon="megaphone" href="/framework/social-post-generator">
    Generate multiple social media posts in a click of button using our social media generator.
  </Card>

  <Card title="Chat assistant" icon="comment" href="/framework/chat-assistant">
    Using Knowledge Graph, our graph-based RAG solution, you can build chat assistants to quickly ask questions using your data sources.
  </Card>

  <Card title="Product description generator" icon="page" href="/framework/product-description-generator">
    Build real-time digital shelves for hundreds of products that are automatically customized for different e-retailers.
  </Card>
</CardGroup>


# Page routes
Source: https://dev.writer.com/framework/page-routes



Framework apps can have multiple pages, with parametrised routes. Pages can be switched from the front-end or the back-end.

## Basic navigation

To enable navigation between *Page* components, they must have a key assigned. This can be set up from the component's settings. Once a key is set up, the page will be accessible at `/#my_page_key`.

### Frontend-triggered page changes

For basic page changes, assign a "Go to page" action to an event handler. For example, if you want to change to page `my_page_key` when a *Button* is clicked, go to the button's settings and under the `click` event select `Go to page "my_page_key"`.

### Backend-triggered page changes

Trigger a page change from the back-end using the `set_page` method of state.

```py theme={null}
# This event handler sends the user to a different page
# depending on time of the day
def handle_click(state):
    from datetime import datetime

    now = datetime.now()
    if now.hour >= 18:
        state.set_page("fun_work_page")
    else:
        state.set_page("work_work_page")
```

## Routes with parameters

You may want to share a URL that links directly to a specific resource within your app. For example, to a specific location or product.

You can do so by specifying parameters in the URL, known as route vars. Framework URLs contain the page key, followed by the route vars and their values. For example, `/#detailPage/product_id=32&country=AR`.

### Adding vars to the URL from the back-end

You can set up variables that are displayed in the URL by passing a dictionary to the `set_route_vars` state method. Use `None` to clear specific keys.

```py theme={null}
# The following code will set the value of product_id
# to the value of the "product" state element
def change_route_vars(state):
    state.set_route_vars({
        "product_id": state["product"]
    })
```

### Retrieving the values

Framework uses the hash portion of the URL to store page and variable data, so even when switching pages or changing variables, the page doesn't reload. To monitor changes to the active URL, set up an event handler for `wf-hashchange` in the *Root* component.

```py theme={null}
# The following event handler reads the product_id route var,
# then assigns its value to the "product" state element.
def handle_hash_change(state, payload):
    route_vars = payload.get("route_vars")
    if not route_vars:
        return
    state["product"] = route_vars.get("product_id")
```


# Product description generator
Source: https://dev.writer.com/framework/product-description-generator



In this tutorial, you'll use the Writer Framework to build a Saturn Snacks product description generator for a variety of food outlets. After adding the initial functionality of the app, you'll also extend the app to include a chart of SEO keyword analysis and the ability for users to add their own food outlet.

<img alt="Finished application" />

## Setting up your project

### Creating a Writer app and getting your API key

From the Home screen, click on **Build an app**.

<img alt="Writer home screen" />

Select Framework as the app type you’d like to create, enabling you to generate keys and build your app with the Writer Framework.

<img alt="App type selection" />

On the next screen, you can edit your Writer application name in the upper left. Underneath “Authenticate with an API key,” click on “Reveal” to see and copy your API key.

### Creating the application

Next, open your terminal and navigate to the directory where you want to create your application directory.

<Steps>
  <Step title="Set the API key environment variable">
    To pass your API key to the Writer Framework, you'll need to set an environment variable called `WRITER_API_KEY`. Here’s how you can set this variable in your terminal session:

    <CodeGroup>
      ```sh On macOS/Linux theme={null}
      export WRITER_API_KEY=[key]
      ```

      ```sh On Windows theme={null}
      set WRITER_API_KEY=[key]
      ```
    </CodeGroup>
  </Step>

  <Step title="Create the application">
    Run the following command to create your application. Replace `product-description-app` with your desired project name and `pdg-tutorial` with the template you wish to use:

    ```
    writer create product-description-app --template=pdg-tutorial
    ```

    This command sets up a new project called `product-description-app` in the specified directory using a template designed for this tutorial.
  </Step>

  <Step title="Edit your project">
    To edit your project, run the below commands. This will bring up the console, where Framework-wide messages and errors will appear, including logs from the API. By default, the Writer Framework Builder is accessible at `localhost:4005`. If that port is in use, you can specify a different port. Open this address in your browser to view your default application setup.

    <CodeGroup>
      ```bash Standard port theme={null}
       writer edit product-description-app
      ```

      ```bash Custom port theme={null}
       writer edit product-description-app --port=3007
      ```
    </CodeGroup>
  </Step>
</Steps>

## Introduction to the application setup

When you first start up the application, you're going to see two main layout items provided by the template:

1. A Header component with the name of the application
2. A Column container that'll house most of the UI of the app

The left column includes a form that has three text inputs and a button. These three text inputs are bound to corresponding state elements. The right column contains a Message component for loading and status messages, as well as an empty Tab container which you'll use to display the product descriptions of the various outlets.

### Code overview

Looking at the code in `main.py`, you'll see that the template already imported the Writer Framework, the AI module, and the product description prompts that you'll use throughout this tutorial.

```python theme={null}
import writer as wf
import writer.ai
from prompts import base_prompts, user_prompt, seo_keywords
```

The prompts are stored in a separate file called `prompts.py`. You are welcome to open this project in the IDE of your choice and modify the prompts however you wish. However, you don't need to make any changes to this file to follow this tutorial.

You'll also see the state initialization:

```python theme={null}
wf.init_state({
   "form": {
       "title": "",
       "description": "",
       "keywords": ""
   },
   "message": "Fill in the inputs and click \"Generate\" to get started.",
})
```

The form elements and the message have been initialized as strings. You'll add to this state dictionary throughout the tutorial.

## Implementing the Product description generator

Your first goal is to generate product descriptions for the various food outlets, with each outlet appearing as a tab to the right of the form.

<img alt="Finished product description tabs" />

### Setting up the code

First, integrate new functionality into your code for generating product descriptions.

<Steps>
  <Step title="Add a private helper method">
    Paste the following method on line 5 after all of the imports to create a helper function for generating product descriptions:

    ```python theme={null}
    def _generate_product_description(base_prompt, product_info):
       prompt = base_prompt.format(**product_info)
       description = writer.ai.complete(prompt)
       return description
    ```

    This function, `_generate_product_description`, accepts a base prompt and the product information from a form on the page. The underscore at the beginning of its name indicates that it's a private method not exposed to the UI.
  </Step>

  <Step title="Initialize additional state elements">
    Update the `wf.init_state()` to include a `product_description` dictionary with visibility control and outlets for descriptions:

    ```python theme={null}
    wf.init_state({
       "form": {
           "title": "",
           "description": "",
           "keywords": ""
       },
       "message": "Fill in the inputs and click \"Generate\" to get started.",
       "product_descriptions": {
           "visible": False,
           "outlets": {}
       }
    })
    ```

    This setup includes a variable `visible` to control whether product description tabs are shown or hidden, and an empty dictionary `outlets` for storing descriptions.
  </Step>

  <Step title="Add a button click handler">
    Paste the following method beneath `_generate_product_description` to handle button clicks and generate descriptions:

    ```python theme={null}
    def handle_click(state):
       state["product_descriptions"]["visible"] = False

       # Loop through all the base prompts to generate versions tailored to each outlet
       for outlet, base_prompt in base_prompts.items():
           state["message"] = f"% Generating product description for {outlet}..."
           product_description = _generate_product_description(base_prompt, state["form"].to_dict())
           state["product_descriptions"]["outlets"][outlet] = product_description

       state["product_descriptions"]["visible"] = True
       state["message"] = ""
    ```

    This handler will loop through each imported base prompt, format it with the form information, and pass it to the helper method. The handler also manages UI interactions, such as displaying and hiding product descriptions and managing loading messages.
  </Step>
</Steps>

### Setting up the user interface

You can now set up the UI to iterate over the product descriptions dictionary and create tabs. Begin by opening the User Interface.

<Steps>
  <Step title="Add and configure the Repeater component">
    In the toolkit, drag a Repeater component from the Other section into the empty Tab Container. Click on the Repeater component to open its component settings. Under Properties, add `@{product_descriptions.outlets}` as the Repeater object to be used for repeating the child components. Replace the default “Key variable name” with `itemId`. You can leave “Value variable name” as `item`.

    <img alt="Repeater settings" />
  </Step>

  <Step title="Add and configure the Tab component">
    From the Layout section of the toolkit, drag a Tab component into the Repeater. Click on the Tab to bring up the component settings and add `@{itemId}` to the Name property to display the outlet name on the tab.

    <img alt="Tab settings" />
  </Step>

  <Step title="Add and configure the Text component">
    Drag a Text component from the Content section of the Toolkit into the Tab. Click on the Text component to open the Component settings and set the Text property to `@{item}`. You may also choose to set “Use Markdown” to “yes.”

    <img alt="Text settings" />
  </Step>

  <Step title="Control the visibility of the Tab container">
    Click on the Tab container to bring up its Component settings. Scroll to the bottom and, under Visibility, click “Custom” and add `product_descriptions.visible` to the Visibility value input.

    <img alt="Tab container visibility settings" />
  </Step>

  <Step title="Wire up the form with the Generate button">
    Click on the Generate button inside the form to bring up its Component settings. In the Events section, select `handle_click` for the `wf-click` event.

    <img alt="Button settings" />
  </Step>

  <Step title="Preview and test the application">
    Click **Preview** in the top toolbar, enter some test data, and click the **Generate** button. You should see a loading message, as well as three example food outlets displayed in the tabs. The loading message should disappear when everything is loaded, and the tab should remain visible once the data has loaded.

    Great work!
  </Step>
</Steps>

## Expanding the application: SEO keyword analysis

You can expand on this application by adding a chart that displays the top ten SEO keywords present in the product descriptions.

### Updating the code

To do this, back in the code, first add the following helper function underneath your ` _generate_product_description` helper method:

```python theme={null}
def _generate_seo_keywords(outlets):
   combined_descriptions = "\n".join(f"{key}: {value}" for key, value in outlets.items())


   # Generate the prompt with the provided descriptions
   prompt = seo_keywords.format(descriptions=combined_descriptions)
   # Strip out whitespace and backticks from the response
   return writer.ai.complete(prompt).strip(" `\n")

```

This method concatenates all of the product descriptions and incorporates them into a prompt in `prompts.py`. It then sends the formatted prompt to the Palmyra LLM using the `complete` method. The prompt not only analyzes the descriptions for SEO keywords, but also outputs a [Plotly.js](/components/plotlygraph) schema object that you can use directly with a Plotly graph component.

With the helper method in place, you can now update the click handler for the button. On line 27, add the following code before the product description visibility is set:

```python theme={null}
# Create the SEO analysis
   state["message"] = "Analyzing SEO keywords..."
   outlets = state["product_descriptions"]["outlets"]
   state["seo_analysis"] = _generate_seo_keywords(outlets)
```

This code sets the loading message and passes all of the product descriptions to the SEO keyword helper method.

### Adding SEO analysis to the UI

To update the UI to display this chart, first drag a new tab from the Layout section of the toolkit into the Tab container. This tab should not be inside of the Repeater, but can be either before or after it. Click on the tab to open the component settings, and change the name to “SEO Analysis.” If you'd like, you can also set the Visibility to “Custom” and set `seo_analysis` as the visibility value.

<img alt="SEO Tab" />

To display the chart, drag a Plotly graph component from the Content section of the toolkit into your new tab. Click on the component to bring up the component settings. The Plotly graph component accepts a graph specification. Add `@{seo_analysis}` to pass the LLM-generated graph specification to the component.

Click preview, add some data to the form, and click generate. You should see a new SEO analysis tab appear with a nicely formatted and labeled chart.

<img alt="SEO analysis tab and chart" />

## Extending the application: user-added outlet

Finally, you can extend this application even further by allowing users to add their own custom food outlet and derive a new description from a custom prompt.

### Adding the new form

Start by building the UI for this new form. From the Layout section of the Toolkit, drag a new Section component into the column where the current form is and drop it above or below it. Click on the Section and change the Name to “Add an outlet.”

To create the inputs for the form, drag a Text Input and a Number Input from the Input section of the Toolkit into the newly created section. Click on the Text Input component to change the Label to “Outlet name.” Click on the Number Input and change the label to “Character max.”

Finally, add a Button from the Other section of the toolkit to the bottom of the new section. Click on the button and change the text to “Add and Generate.” You can also add `laps` or another [Material Symbols](https://fonts.google.com/icons) ID to the Icon input if you wish.

<img alt="Add outlet form" />

### Updating the code

In the code, you next need to add corresponding state elements for the new form components to `wf.init_state()`. Add the following to the state dictionary:

```python theme={null}
"outlet_form": {
       "name": "",
       "character_max": "",
   },
```

Don't forget to check your commas when adding to the state dictionary. Your completed state should look like this:

```python theme={null}
wf.init_state({
   "form": {
       "title": "",
       "description": "",
       "keywords": ""
   },
   "outlet_form": {
       "name": "",
       "character_max": "",
   },
   "message": "Fill in the inputs and click \"Generate\" to get started.",
   "product_descriptions": {
       "visible": False,
       "outlets": {}
   }
})
```

The `outlet_form` state elements will bind to the form elements.

Next, add the click handler for the new button. Copy and paste this `handle_add_outlet` method into the code under the `handle_click` method:

```python theme={null}
def handle_add_outlet(state):
   # Create a new base prompt for the new outlet
   new_outlet_name = state["outlet_form"]["name"]
   product_info = {**state["outlet_form"].to_dict(), **state["form"].to_dict()}
   base_prompt = user_prompt.format(**product_info)


   # Add the new base prompt to the base_prompts dictionary
   base_prompts[new_outlet_name] = base_prompt


   # Generate the product description for the new outlet
   state["message"] = f"% Generating product description for {new_outlet_name}..."
   product_description = _generate_product_description(base_prompt, state["form"].to_dict())
   state["product_descriptions"]["outlets"][new_outlet_name] = product_description


   # Update the SEO analysis
   state["message"] = "Updating SEO analysis..."
   outlets = state["product_descriptions"]["outlets"]
   state["seo_analysis"] = _generate_seo_keywords(outlets)


   state["message"] = ""
```

This method formats the input from both forms into the imported `user_prompt` and adds the formatted prompt to the `base_prompts` dictionary. It then generates the product description for the new food outlet, updates the SEO analysis, and clears the status message.

### Binding the elements and handler to the UI

Finalize your setup by binding the state elements and configuring the click handler to the UI components.

<Steps>
  <Step title="Bind text inputs to state elements">
    * **Outlet Name**: Click on the “Outlet name” Text Input component. In the Binding section of the component settings, set the state element to `outlet_form.name`.
    * **Character Max**: Move to the “Character max” Text Input. Update its state element binding to `outlet_form.character_max`.
  </Step>

  <Step title="Assign click handler to button">
    Click on the **Add and Generate** Button. In the Events section of the component settings, select `handle_add_outlet` for the `wf-click` event.
  </Step>

  <Step title="Configure form visibility">
    To conditionally display the form based on whether descriptions have been generated, click on the Section containing the form. In the Visibility section, choose “Custom” and set `product_descriptions.visible` as the “Visibility value.”
  </Step>
</Steps>

### Testing the finished product

To see the result of your hard work, click **Preview** in the top toolbar, enter some information into the original product description form, and click **Generate**. The “Add an outlet” form should appear once the product descriptions have been generated. Add a new example outlet name and a character max and click “Add and Generate.” You should see a new tab appear with your new outlet, as well as an updated SEO analysis chart.

<img alt="Finished application" />

You can add whatever additional form inputs you wish to the outlet form, but be sure to update `user_prompt` in the `prompts.py` file using your favorite editor.

## Conclusion

You’ve now built a full application with the Writer Framework and the Writer AI module. Congratulations! This application not only demonstrates the platform's capabilities but also provides a foundation on which you can build more complex applications. To learn more, explore the rest of the Writer Framework documentation and the API documentation.


# Quickstart
Source: https://dev.writer.com/framework/quickstart



## Overview

In this guide, you'll learn how to get started with the Writer Framework by building a simple "Hello, World" application. This beginner-friendly guide will help you install the necessary tools, set up an app, and deploy it to the Writer Cloud.

## Step 1: Install Writer Framework and run the demo app

<Tip>
  Writer Framework works on Linux, Mac, and Windows. It supports Python versions 3.9.2 through 3.12. We recommend using a virtual environment.
</Tip>

<Warning>
  Python 3.13 is not yet supported.
</Warning>

<Steps>
  <Step title="Install Writer Framework">
    Use the following command to install the Writer Framework using `pip`. Open your command line interface and type:

    ```bash theme={null}
    pip install writer
    ```

    This will download and install the Writer Framework and all its required dependencies.
  </Step>

  <Step title="Test installation with demo application">
    Once the installation is complete, you can verify that everything is set up correctly by running the built-in demo application. Navigate to your desired directory and type:

    ```bash theme={null}
    writer hello
    ```

    This command creates a subfolder named **hello** and launches the Writer Framework, opening a visual editor accessible through a local URL displayed in your command line. This demo app illustrates the different components available in Writer Framework, helping you get familiar with the setup.
  </Step>
</Steps>

## Step 2: Create a new framework app

Now that we've tested the setup, let's create our first "Hello, World" app using the `ai-starter` template. This template showcases how quickly you can build AI-powered applications with the Writer Framework.

Use the following command to generate the app structure:

```sh theme={null}
writer create hello-world --template=ai-starter
```

This will create a new app folder, **hello-world**, with the following structure:

1. `main.py` - The entry point for the app. You can import anything you need from here.
2. `.wf/` - This folder contains the UI component declarations. Maintained by the Writer Framework's visual editor.
3. `static/` - This folder contains front-end static files which you might want to distribute with your app, such as images and stylesheets.

## Step 3: Start the visual editor

To start building and customizing your app visually, use the Writer Framework’s editor. This browser-based interface allows you to drag components, arrange layouts, and modify settings in real-time.

```sh theme={null}
writer edit hello-world
```

After running this command, you'll see a local URL displayed in your command line. Open this URL in a web browser to access the editor.

<Frame>
  <img />
</Frame>

<Steps>
  <Step title="Launch the Editor">
    In your terminal, enter the following command to open the editor:

    ```sh theme={null}
    writer edit hello-world
    ```

    This command opens the Writer Framework editor in your browser, where you’ll see a live preview of your app. The editor interface includes a **component library** on the left, a **canvas** in the center for building layouts, and a **settings panel** on the right for customizing selected components.

    <Info>
      The editor starts by default on port 4005. If you launch multiple editors in parallel and do not specify a port, Writer Framework will automatically assign the next port until reaching the limit of 4099.
    </Info>

    <Warning>
      It's not recommended to expose the Framework to the Internet. If you need to access the editor remotely, we recommend setting up an SSH tunnel. By default, requests from non-local origins are rejected as a security measure to protect against drive-by attacks.

      If you need to disable this protection, use the flag `--enable-remote-edit`.
    </Warning>
  </Step>

  <Step title="Add Components to Your Canvas">
    On the left sidebar, browse through the **Component Library** and try dragging a few components onto the **Canvas**. For example, you can add a **Text** component to display "Hello, World!" or try other components to see how they work together.

    Experiment with arranging these components on the canvas to create a simple layout.

    <Frame>
      <img />
    </Frame>
  </Step>

  <Step title="Customize Component Settings">
    With a component selected, look at the **Settings Panel** on the right side. Here, you can edit properties like text content, colors, and alignment to see how it changes the appearance and behavior of the component on the canvas.

    This panel allows you to customize components without writing any code, giving you a quick and visual way to build a frontend.
  </Step>

  <Step title="Change the App Name in main.py">
    To personalize your app further, open `main.py` in the root of your project folder. Locate the following line:

    ```python theme={null}
    wf.init_state({
        "my_app": {
            "title": "AI STARTER"
        },
    })
    ```

    Change `"AI STARTER"` to something unique, like `"My First Writer App"`. Save the file, and you’ll see the updated name reflected immediately in the editor.

    <Frame>
      <img />
    </Frame>
  </Step>
</Steps>

For now, this simple exploration gives you an idea of how the framework enables rapid frontend building without code. In later tutorials, like creating a [Chat assistant](/framework/chat-assistant) or a [Social post generator](/framework/social-post-generator), you’ll explore more advanced components and layouts using this visual editor.

## Step 4: Run the app locally

When your app is ready, execute the `run` command, which will allow others to run, but not edit, your Framework app.

```sh theme={null}
writer run hello-world
```

<Info>
  Your app starts by default on port 3005. If you launch multiple apps in parallel and do not specify a port, Writer Framework will automatically assign the next port until reaching the limit of 3099.
</Info>

You can specify a port and host. Specifying `--host 0.0.0.0` enables you to share your application in your local network.

```sh theme={null}
writer run hello-world --port 5000 --host 0.0.0.0
```

## Conclusion

Congratulations! You’ve set up and configured your first app with the Writer Framework. Now, try building a [Chat assistant](/framework/chat-assistant) or a [Social post generator](/framework/social-post-generator) to put these skills into action.


# Quickstart tutorial
Source: https://dev.writer.com/framework/quickstart-tutorial



In this tutorial, we'll guide you through creating a logistic regression visualisation tool using Framework. Logistic regression is a fundamental technique in machine learning for binary classification tasks, and visualising its decision boundary can provide valuable insights into the model's behaviour.

First, make sure you have Framework installed. You can install it via pip:

```bash theme={null}
pip install "writer"
```

Now, let's get started with creating our logistic regression visualisation tool. We'll break down the process into the following steps:

1. Setup Project
2. UI Creation
3. App state and bindings
4. Python implementation
5. Troubleshooting

So, without further ado, let's jump into it.

## Project Setup

To create our project, we will use the following commands:

```bash theme={null}
writer create logistic_regression
cd logistic_regression
```

Commands will create basic template of app project with initial file structure.
In this project, we will be using the scikit-learn package for logistic regression, so let's install it before we start.
Create a file `requirements.txt` and add the following line:

```
scikit-learn==1.4.0
```

After that, we can install our requirements.

```bash theme={null}
pip install -r requirements.txt
```

Once this is done, we can finally run the Framework editor using the command:

```bash theme={null}
writer edit .
```

This will run our Framework instance. Runtime logs can be observed in the terminal, and the app is available at [http://localhost:4005](http://localhost:4005).

<img alt="newly created application" />

## UI Creation

By default, Framework creates a simple application with a counter. To keep things easy, let's remove the contents from columns to make space for our new application. If you're unsure where to click to select a specific component on the screen, you can always use the `Component Tree` on the bottom left of the screen. The app should look something like this when you finish.

<img alt="empty app" />

This app will be made up of 2 columns, one with controls for our plot and the second with the plot itself. Half of the screen is way too much for controls, and the plot will be really small this way. To change the proportions of the columns, change the value of `Width (factor)` in the left column to `0.5`.

<img alt="width controls" />

This way, it will take just `1/3` of the screen. Proportions are calculated relative to each other. Each *Column*, by default, has the value of this factor set to 1. So, when we set the left column to 0.5 and the right to 1, we will get a relation between column sizes of 1:2.

<img alt="placing elements" />

Now let's add the rest of the components:

* To the right column, add a *Plotly Graph*.
* To the left:
  * 3 x *Slider Input*
  * *Dropdown Input*
  * *Button*.
* To the free space in the header, let's place a *Message* component.

<img alt="ui boilerplate" />

Now we can configure components with some static settings. You can find all the configuration options in right sidebar. Sidebar will apear after selecting the component using mouse click. Starting with slider inputs, let's set all 3 sliders configuration values to the following values:

|    Property   | Value (Slider 1) | Value (Slider 2) | Value (Slider 3)  |
| :-----------: | :--------------- | :--------------- | :---------------- |
|     Label     | Number of groups | Number of points | Cluster deviation |
| Minimum value | 2                | 50               | 0                 |
| Maximum value | 10               | 1000             | 10                |
|   Step size   | 1                | 1                | 0.1               |

Then for the dropdown, we will set:

| Property | Value                                                  |
| :------: | :----------------------------------------------------- |
|   Label  | Type                                                   |
|  Options | set `JSON` and paste:                                  |
|          | `{"ovr": "One vs Rest", "multinomial": "Multinomial"}` |

And in the end lets rename the button.

| Property | Value      |
| :------: | :--------- |
|   Text   | Regenerate |

## App State and Bindings

It is time to create the application's initial state. Let's open the code editor. In this tutorial, we will be using the built-in code editor, which can be found by clicking on the 'Code' button at the top of the screen. However, if you prefer using your favourite editor, you can simply edit the `main.py` file, and the app will auto-refresh every time the file is changed.

<img alt="code editor" />

Let's remove all code from there and start with:

```python theme={null}
import writer as wf
import plotly.graph_objects as go
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_blobs

initial_state = wf.init_state({
    "my_app": {
        "title": "Logistic Regression Visualiser"
    },
    "message": None,
    "figure": None,
    "multi_class": "ovr",
    "number_of_groups": 2,
    "number_of_points": 50,
    "cluster_std": 2,
})
```

For now, only the Framework import is needed, but the rest will be used later on. Notice that after pasting this code into the editor, when we click on `Save and run`, the header of our application will immediately change to "Logistic regression visualiser". This is because the Header has in its text property the value `@{my_app.title}`, which is a template syntax that uses a value from the state.

The rest of the keys and values from the `initial_state` are names and initial values for components that will be used for communication between the app UI and the back-end. Assigning those keys to components is called binding, and it is done by clicking on each component and filling property "State element" in the "Binding" menu. Let's use these bindings for our 3 *Slider Input* components:

|    Property   | Value (Slider 1)   | Value (Slider 2)   | Value (Slider 3) |
| :-----------: | :----------------- | :----------------- | :--------------- |
| State element | `number_of_groups` | `number_of_points` | `cluster_std`    |

For the *Dropdown Input*:

|    Property   | Value         |
| :-----------: | :------------ |
| State element | `multi_class` |

If you closely follow my steps, the massage component can be hidden behind the right bar. Use the `Component Tree` to click the element and open the `Properties` menu.

*Message* component doesen't have bindings but we still can use template syntax to use data from the state inside of a `Message`. Additionaly we are able to use this data to control the visibility of the component.

|     Property     | Value        |
| :--------------: | :----------- |
|      Message     | `@{message}` |
|    Visibility    | `custom`     |
| Visibility value | `message`    |

This way, the *Message* component will show only if there is a message to display.

For *Plotly Graph* we use template syntax again:

|       Property      | Value       |
| :-----------------: | :---------- |
| Graph specification | `@{figure}` |

Congratulations! You have just connected all components to the application state. Nothing is happening yet. Now, we are ready to make it alive. It's time for python!

## Python implementation

Let's create a function that will update our application based on inputs and call it immediately.

```python theme={null}
def update(state):
   state["message"] = "Hello, world!"

update(initial_state)
```

Notice that *Message* showed up, and it is now displaying the message "Hello, World!". To have better access to state parameters and to make it clear on which parameters our function depends, we define them at the top of the function. Notice that some of them need to be mapped to appropriate types. The *Slider Input* returns float values by default, so here, as we will need integers, we cast values to int.

```python theme={null}
def update(state):
    cluster_std = state['cluster_std']
    multi_class = state['multi_class']
    number_of_points = int(state['number_of_points'])
    number_of_groups = int(state['number_of_groups'])
```

In this example, we create a logistic regression visualisation, but the algorithm itself is not in the scope of this tutorial. So we will just use the basic function from the `scikit-learn` library.

```python theme={null}
    X, y = make_blobs(
        n_samples=number_of_points,
        n_features=2,
        cluster_std=cluster_std,
        centers=groups
    )

    clf = LogisticRegression(
        solver="sag",
        max_iter=1000,
        random_state=42,
        multi_class=multi_class
    ).fit(X, y)

    coef = clf.coef_
    intercept = clf.intercept_
    score = clf.score(X, y)
```

`make_blobs` will generate `number_of_points` points on a 2D space that have a cluster deviation set to `cluster_std` and the number of groups defined by `number_of_groups`. After that, we generate a LogisticRegression, and we take parameters we need. For better knowledge about the process, let's display the training score as a message.

```python theme={null}
    state["message"] = "training score : %.3f (%s)" % (score, multi_class)
```

The algorithm will generate one or many lines depending on how many groups we have. To be able to draw those lines, let's create a helper function:

```python theme={null}
def _line(x0, coef, intercept, c):
    return (-(x0 * coef[c, 0]) - intercept[c]) / coef[c, 1]
```

This function is a helper function meant to be used on the back-end. The underscore at the beginning of its name tells Framework that this function is private, and the front-end won't know about its existence.

To make the plot more readable, let's quickly define some colours for our plots:

```python theme={null}
COLOR = {
    0: '#3c64fa',
    1: '#00eba8',
    2: '#5a677c',
    3: '#ff8866',
    4: '#d4b2f7',
    5: '#c3e6ff',
    6: '#045758',
    7: '#001435',
    8: '#ec3d10',
    9: '#38006a'
}
```

Now, let's create a plot for our logistic regressions. For that, we will use `plotly.graph_objects` to have full control over what will be included in our plot.

```python theme={null}
    data = []
    for i in range(number_of_groups):
        data.append(
            go.Scatter(
                x=X[y == i][:, 0],
                y=X[y==i][:, 1],
                mode='markers',
                name='Group '+str(i),
                hoverinfo='none',
                marker=dict(
                    color=COLOR[i],
                    symbol='circle',
                    size=10
                )
            )
        )

    for i in range(1 if number_of_groups < 3 else number_of_groups):
        data.append(go.Scatter(
            x=[-20, 20],
            y=[
                _line(-20, coef, intercept, i),
                _line(20, coef, intercept, i)
            ],
            mode='lines', 
            line=dict(color=COLOR[i], width=2),
            name='Logistic Regression'
        ))

    layout = go.Layout(
        width=700,height=700,
        hovermode='closest', hoverdistance=1,
        xaxis=dict(
            title='Feature 1',
            range=[-20,20],
            fixedrange=True,
            constrain="domain",
            scaleanchor="y",
            scaleratio=1
        ),
        yaxis=dict(
            title='Feature 2',
            range=[-20,20],
            fixedrange=True,
            constrain="domain"
        ),
        paper_bgcolor='#FFFFFF',
        margin=dict(l=30, r=30, t=30, b=30),
    )

    fig = go.Figure(data=data, layout=layout)
    state['figure'] = fig
```

After saving and running the code, we should get something like this:

But as you could notice, when we change our slider values, nothing happens. It's because our function is currently called only once on app initialisation and not after input changes. To change this behaviour, let's set the event handler for `wf-number-change` in all slider inputs to `update`, which is the name of our function in the Python code. Notice that `_line` is not visible there because it's private to the back-end. If we would change its name to `line`, then it would be visible on this list.

Set also event handler for dropdown input `wf-option-change` and `wf-click` in the button, also to `update`.

And done! You can have fun with your new application. Feel free to modify and play with other options.

Final code for the application should look something like this:

```python theme={null}
import writer as wf
import plotly.graph_objects as go
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_blobs

COLOR = {
    0: '#3c64fa',
    1: '#00eba8',
    2: '#5a677c',
    3: '#ff8866',
    4: '#d4b2f7',
    5: '#c3e6ff',
    6: '#045758',
    7: '#001435',
    8: '#ec3d10',
    9: '#38006a'
}

def _line(x0, coef, intercept, c):
    return (-(x0 * coef[c, 0]) - intercept[c]) / coef[c, 1]

def update(state):
    cluster_std = state['cluster_std']
    multi_class = state['multi_class']
    number_of_points = int(state['number_of_points'])
    number_of_groups = int(state['number_of_groups'])

    X, y = make_blobs(
        n_samples=number_of_points,
        n_features=2,
        cluster_std=cluster_std,
        centers=number_of_groups
    )

    clf = LogisticRegression(
        solver="sag",
        max_iter=1000,
        random_state=42,
        multi_class=multi_class
    ).fit(X, y)

    coef = clf.coef_
    intercept = clf.intercept_
    score = clf.score(X, y)

    state["message"] = "training score : %.3f (%s)" % (score, multi_class)

    data = []
    for i in range(number_of_groups):
        data.append(
            go.Scatter(
                x=X[y == i][:, 0],
                y=X[y==i][:, 1],
                mode='markers',
                name='Group '+str(i),
                hoverinfo='none',
                marker=dict(
                    color=COLOR[i],
                    symbol='circle',
                    size=10
                )
            )
        )

    for i in range(1 if number_of_groups < 3 else number_of_groups):
        data.append(go.Scatter(
            x=[-20, 20],
            y=[
                _line(-20, coef, intercept, i),
                _line(20, coef, intercept, i)
            ],
            mode='lines', 
            line=dict(color=COLOR[i], width=2),
            name='Logistic Regression'
        ))

    layout = go.Layout(
        width=700,height=700,
        hovermode='closest', hoverdistance=1,
        xaxis=dict(
            title='Feature 1',
            range=[-20,20],
            fixedrange=True,
            constrain="domain",
            scaleanchor="y",
            scaleratio=1
        ),
        yaxis=dict(
            title='Feature 2',
            range=[-20,20],
            fixedrange=True,
            constrain="domain"
        ),
        paper_bgcolor='#EEEEEE',
        margin=dict(l=30, r=30, t=30, b=30),
    )

    fig = go.Figure(data=data, layout=layout)
    state['figure'] = fig


initial_state = wf.init_state({
    "my_app": {
        "title": "Logistic regression visualizer"
    },
    "message": None,
    "figure": None,
    "multi_class": "ovr",
    "number_of_groups": 2,
    "number_of_points": 50,
    "cluster_std": 2,
})

update(initial_state)
```

## Troubleshooting

### Errors

When your code has an error, you will be notified with a notification in the app, and also in the console, you can find useful logs.

### Debugging

To check some intermediate values in your Python code, you can just use `print()` function. All logs will be available in the terminal.

## Conclusion

Congratulations! You've successfully created a logistic regression visualisation tool using Framework. You can further customise and enhance this tool to suit your specific needs.

## Additional Resources

* [Scikit-learn Documentation](https://scikit-learn.org/stable/)
* [Plotly Documentation](https://plotly.com/python/)

Feel free to explore these resources to deepen your understanding and expand your capabilities.


# Release notes generator
Source: https://dev.writer.com/framework/release-notes-generator



In this tutorial, you'll build a release notes generator using the Writer Framework. This application will help you generate release notes as formatted HTML for software updates based on user-provided data as a CSV file. You can check out the [finished code on GitHub](https://github.com/writer/framework-tutorials/tree/main/release-notes-generator/end) to see what you'll be building.

The application consists of two main parts: the backend, which processes the uploaded CSV file and generates the release notes, and the frontend, which displays the release notes and allows users to download them.

<img alt="Finished release notes generator application" />

## Setting up your project

### Creating a Writer app and getting your API key

From the Home screen, click on **Build an app**.

<img alt="Writer home screen" />

Select Framework as the app type you want to create, enabling you to generate keys and build your app with the Writer Framework.

<img alt="App type selection" />

On the next screen, you can edit your Writer application name at the top left. Underneath "Authenticate with an API key," click "Reveal" to view and copy your API key.

### Creating the application

Next, open your terminal and navigate to the directory where you want to create your application directory.

<Steps>
  <Step title="Set the API key environment variable">
    To pass your API key to the Writer Framework, you'll need to set an environment variable called `WRITER_API_KEY`. Here's how you can set this variable in your terminal session:

    <CodeGroup>
      ```sh On macOS/Linux theme={null}
      export WRITER_API_KEY=[key]
      ```

      ```sh On Windows theme={null}
      set WRITER_API_KEY=[key]
      ```
    </CodeGroup>
  </Step>

  <Step title="Clone the application">
    Run the following command to clone the `framework-tutorials` repo and navigate to the folder containing the starting point for this application.

    ```
    git clone https://github.com/writer/framework-tutorials.git
    cd framework-tutorials/release-notes-generator/start
    ```
  </Step>

  <Step title="Edit your project">
    To edit your project, run the following commands. This will bring up the console, which displays Framework-wide messages and errors, including logs from the API. By default, the Writer Framework Builder is accessible at `localhost:4005`. If this port is in use, you can specify a different port. Open this address in your browser to view your default application setup.

    <CodeGroup>
      ```bash Standard port theme={null}
       writer edit .
      ```

      ```bash Custom port theme={null}
       writer edit . --port=3007
      ```
    </CodeGroup>
  </Step>
</Steps>

## Introduction to the application setup

The template includes some basic code, UI setup, and files to help you get started.

### Included files

The files `prompts.py` and `html_template.py` contain helper functions for generating prompts for the AI model and formatting the output HTML, respectively.

In the `sample-input` folder, you'll find a sample CSV file that you can use to test the application.

Finally, the `custom.css` file in the `static` folder contains custom CSS styles for the application.

### Dependency imports

In `main.py`, you'll see that the dependencies are already imported at the top:

```python theme={null}
import writer as wf
import writer.ai
import pandas as pd
from prompts import get_release_notes_summary_prompt, get_release_notes_desc_prompt, get_category_prompt
from html_template import format_output
```

These dependencies include the Writer Framework, the Writer AI module, and pandas for data manipulation.

### Initial UI

The template includes a basic UI setup, including a Page component with a Header component. The Header component also includes an Image. If you want to change the logo image, you can replace the `logo_image_path` variable in the state with the path to your desired image in the `static` folder.

## Initializing the application state

First, in `main.py`, set up the initial state for the application. This state will store the application's title, logo image path, file data, metrics, and processing status for each step. You'll also import a custom CSS file to style the application and create a placeholder DataFrame.

<Steps>
  <Step title="Create initial DataFrame">
    Create a placeholder DataFrame on the line above `wf.init_state`:

    ```python theme={null}
    placeholder_data = {
        'Description': ['Description 1', 'Description 2', 'Description 3'],
        'Label': ['Label 1', 'Label 2', 'Label 3']
    }
    initial_df = pd.DataFrame(placeholder_data)
    ```
  </Step>

  <Step title="Initialize state">
    Update the initial state for the application at the bottom of `main.py`:

    ```python theme={null}
    initial_state = wf.init_state({
        "my_app": {"title": "RELEASE NOTES GENERATOR"},
        "logo_image_path": 'static/Writer_Logo_black.svg',
        "file": {"name": "", "file_path": ""},
        "metrics": {"new_features": 0, "caveats": 0, "fixed_issues": 0, "total": 0},
        "step1": {
            "raw_csv": initial_df,
            "completed": "no",
            "generate-button-state": "yes",
            "processing-message": None,
            "styled-table": "<h3>csv table</h3>"
        },
        "step2": {
            "release-notes": None,
            "completed": "no",
            "formatted-release-notes": "notes should go here"
        },
    })
    ```
  </Step>

  <Step title="Import custom CSS">
    Import the custom CSS file below the initial state setup:

    ```python theme={null}
    initial_state.import_stylesheet(".description, .list, .summary, .category ", "/static/custom.css")
    ```
  </Step>
</Steps>

## Building the file upload functionality

First, you'll build the file upload feature. Note that `prompts.py`, `html_template.py`, and `custom.css` are provided in the starting point for the application. There is also a sample CSV file in the `sample-input` folder that you can use to test the application.

### Implementing the file upload handler

To handle file uploads, you'll create a function in `main.py` that reads the uploaded CSV file, processes the data, and stores it in the application state.

<Steps>
  <Step title="Implementing a file upload handler">
    In `main.py`, create a function to handle file uploads. This function will read the uploaded CSV file, process the data, and store it in the application state.

    ```python theme={null}
    def onchangefile_handler(state, payload):
        uploaded_file = payload[0]
        name = uploaded_file.get("name")
        state["file"]["name"] = name
        state["step1"]["processing-message"] = f'+File {name} uploaded successfully.'
        state["file"]["file_path"] = f"data/{name}"
        file_data = uploaded_file.get("data")
        with open(f"data/{name}", "wb") as file_handle:
            file_handle.write(file_data)
        
        data = pd.read_csv(state["file"]["file_path"])
        df = pd.DataFrame(data)
        state["step1"]["raw_csv"] = df
        state["step1"]["generate-button-state"] = "no"
    ```
  </Step>

  <Step title="Create CSV to DataFrame converter">
    Define a function to convert the CSV file to a DataFrame:

    ```python theme={null}
    def _raw_csv_to_df(state):
        data = pd.read_csv(state["file"]["file_path"])
        df = pd.DataFrame(data)
        return df
    ```
  </Step>
</Steps>

### Displaying the uploaded CSV file

Next, you'll display the uploaded CSV file in the application UI.

<Steps>
  <Step title="Creating a Step Container">
    Add a Step Container component to the Page. This will contain the two steps for the application.
  </Step>

  <Step title="Create Step components">
    Drag two Step components into the Step Container. Name the first one "Load CSV file" and the second "Release notes".
  </Step>

  <Step title="Configure first Step component">
    Click on the the first Step component to select it and bring up the Properties pane. Set "Completed" to `@{step1.completed}`. This state reference will contain either "yes" or "no" based on the completion status of the step.
  </Step>

  <Step title="Add Message component">
    Within this Step, add a Message component with the message set to `@{step1.processing-message}`. Scroll down to the Visibility section of the settings. Select "Custom" and set the condition to `step1.processing-message`.
  </Step>

  <Step title="Create three-column layout">
    Add a Column Container component and add three Column components. For the first column, set the width to 0.5. For the third column, set "Content alignment (H)" to "Left" and "Content alignment (V)" to "Bottom."
  </Step>

  <Step title="Add file input">
    In the middle column, place a File Input component labeled "Please upload your CSV file". Set its `wf-file-change` handler to `onchangefile_handler`.
  </Step>

  <Step title="Add generate button">
    In the third column, add a Button labeled "Generate release notes". Set its "Disabled" property to `@{step1.generate-button-state}` and its "Icon" property to `laps`.
  </Step>

  <Step title="Add Raw CSV section">
    Under the columns, create a Section component and set the title to "Raw CSV".
  </Step>

  <Step title="Add DataFrame component">
    In this section, add a DataFrame component to display the raw CSV data. Configure its properties to use `@{step1.raw_csv}` as the data source. Toggle "Enable download," "Use Markdown," and "Wrap text" to "yes". Set the Separator color to `#d4b2f7` using the CSS tab.
  </Step>
</Steps>

Your application should now look like this:

<img alt="Release notes generator UI" />

When using the sample data located in `sample-input/test-data.csv`, the Raw CSV section will display the uploaded CSV file:

<img alt="Raw CSV section" />

## Generating release notes

Now that you've set up the file upload functionality, you can generate release notes based on the uploaded CSV file.

### Defining text completion functions

Using the prompts provided, define functions to get the category, release notes summary, and release notes description using AI completion. You'll use these functions to process the uploaded CSV file and generate release notes.

<Steps>
  <Step title="Create category function">
    Define a function to get the category using AI completion:

    ```python theme={null}
    def _get_category(desc, label):
        prompt = get_category_prompt(desc, label)
        label = writer.ai.complete(prompt)
        return label
    ```
  </Step>

  <Step title="Create summary function">
    Define a function to get the release notes summary:

    ```python theme={null}
    def _get_release_notes_summary(label, desc):
        prompt = get_release_notes_summary_prompt(label, desc)
        formatted_desc = writer.ai.complete(prompt)
        return formatted_desc
    ```
  </Step>

  <Step title="Create description function">
    Define a function to get the release notes description:

    ```python theme={null}
    def _get_release_notes_desc(label, desc):
        prompt = get_release_notes_desc_prompt(label, desc)
        formatted_desc = writer.ai.complete(prompt)
        return formatted_desc
    ```
  </Step>
</Steps>

### Implementing the generate functionality

You'll next implement the ability to process the CSV and generate release notes.

<Steps>
  <Step title="Create category HTML">
    Define a function to generate HTML for the categories:

    ```python theme={null}
    def _create_df_for_category(df,state):
        unique_categories = df['Primary-Category']
        formatted_output_list = list()
        for category in set(unique_categories):
            df_category = df[df['Primary-Category']==category]
            categories = {" New Feature": "new_features", " Caveat": "caveats", " Fixed Issue": "fixed_issues" }
            curr_category = categories[category]
            state["metrics"][curr_category]= df_category.shape[0]
            formatted_output = format_output(category,df_category)
            formatted_output_list.append(formatted_output)
        return "".join(formatted_output_list)
    ```
  </Step>

  <Step title="Create HTML file writer">
    Define a function to write HTML to a file:

    ```python theme={null}
    def _write_html_to_file(html):
        with open("data/output-html.html", "w") as file_handle:
            file_handle.write(html)
    ```
  </Step>

  <Step title="Implement generate button handler">
    Next, create a function to handle the generate button click. This function will process the uploaded CSV file, generate release notes, and store the formatted output in the application state.

    ```python theme={null}
    def handle_generate_button_click(state):
        state["step1"]["generate-button-state"] = "yes"
        state["step1"]["processing-message-isVisible"] = True
        state["step1"]["processing-message"] = "%Hang tight, preparing to process your file"

        notes_counter = 0
        df = _raw_csv_to_df(state)
        csv_row_count = df.shape[0]
        for index, row in df.iterrows():
            df.at[index,"Primary-Category"] = _get_category(label=row["Labels"], desc=row["Description"])
            df.at[index,"Release-Notes-Summary"] = _get_release_notes_summary(label=row["Labels"], desc=row["Description"])
            df.at[index,"Release-Notes-Description"] = _get_release_notes_desc(label=row["Labels"], desc=row["Description"])
            notes_counter += 1 
            state["step1"]["processing-message"] = f'%Processing {notes_counter} of {csv_row_count} Release Notes'
        
        df_temp = df[["Primary-Category","Release-Notes-Summary","Release-Notes-Description"]]
        df_sorted = df_temp.sort_values(by='Primary-Category')

        state["step2"]["release-notes"] = df_sorted
        state["step1"]["completed"] = "yes"
        state["step1"]["processing-message"] = ""

        html = _create_df_for_category(df_sorted,state)
        _write_html_to_file(html)
        state["step2"]["formatted-release-notes"] = html
        state["metrics"]["total"] = df_sorted.shape[0]

        state["step1"]["generate-button-state"] = "no"
    ```
  </Step>

  <Step title="Bind button handler to Generate button">
    Finally, click on the "Generate release notes" button in the UI builder and set its `wf-click` handler to `handle_generate_button_click`.
  </Step>
</Steps>

## Displaying the release notes

Now that you've generated the release notes, you can display them in the application UI.

### Implementing helper functions

Define helper functions to handle back button clicks, write HTML to a file and download the HTML file.

<Steps>
  <Step title="Create back button handler">
    Define a function to handle the back button click:

    ```python theme={null}
    def handle_back_button_click(state):
        state["step1"]["completed"] = "no"
    ```
  </Step>

  <Step title="Create file download handler">
    Define a function to handle downloading the HTML file:

    ```python theme={null}
    def handle_file_download(state):
        html_data = wf.pack_file("data/output-html.html","text/html")
        file_name = "output-html.html"
        state.file_download(html_data,file_name)
    ```
  </Step>
</Steps>

### Building the initial release notes UI

Next, you'll build the UI for the "Release notes" step.

<Steps>
  <Step title="Select 'Release notes' Step">
    To display the Release notes Step component, you'll need to double-click on it.
  </Step>

  <Step title="Add Separator">
    Inside of the Step component, add a Separator component.
  </Step>

  <Step title="Add Columns components">
    Below the Separator, add a Column Container component and a single Column component.
  </Step>

  <Step title="Add Back button">
    In the column, add a Button. Set its text to "Back" and set its `wf-click handler` to `handle_back_button_click`. Set the "Icon" property to `arrow_back`.
  </Step>
</Steps>

### Buiding tabs for Release notes display

Below the Back button, add a Tab Container component and two Tab components. Name them "Formatted release notes" and "Release notes".

#### Formatted release notes tab

In the first tab, you'll display the formatted release notes.

<Steps>
  <Step title="Add HTML component">
    In the first tab, add an HTML Element component. Set the "Element" property `div` and the "Styles" property to the following object:

    ```
    {
        "padding": "16px",
        "min-height": "64px",
        "min-width": "64px",
        "border-radius": "8px",
        "background": "white",
    }
    ```

    Finally, set the "HTML inside" property to `@{step2.formatted-release-notes}`.
  </Step>

  <Step title="Create three-column layout">
    Inside thie HTML Element component, create a three-column layout using a Column Container component and three Column components.
  </Step>

  <Step title="Add Metric components">
    In each column, add three Metric components to display new features, caveats, and fixed issues, respectively.
    Set the "Name" of these components to a single space to remove the placeholder text: ` `. Then, set the values of these components to `@{metrics.new_features}`, `@{metrics.caveats}`, and `@{metrics.fixed_issues}`. Finally, set the "Note" text to "+New Features", "+Caveats", and "+Fixed Issues" respectively. The "+" sign will display styling that indicates a positive message.
  </Step>

  <Step title="Add download button">
    Under the columns, add a Button component. Set its "Text" to "Download HTML" and its "Icon" to `download`. Then, set the `wf-click` handler to `handle_file_download`.
  </Step>
</Steps>

The Formatted release notes tab should look like this when using the sample data:

<img alt="Formatted release notes tab" />

#### Release notes tab

Finally, you'll add a Dataframe component to the second tab to display the detailed release notes.

<Steps>
  <Step title="Add Metric component">
    In the second tab, start with a Metric component to show the total number of release notes generated. Set the "Name" to "Number of release notes generated" and the "Value" to `@{metrics.total}`. Delete the default value for "Note".
  </Step>

  <Step title="Add DataFrame component">
    Follow this with a DataFrame component to display the detailed release notes, setting the "Data" property to `@{step2.release-notes}`. Configure it for text wrapping, downloading, and searching capabilities. Set the Separator color to `#d4b2f7`.
  </Step>
</Steps>

The final Release notes section should look like this:

<img alt="Release notes final UI" />

You can [see the finished code on GitHub](https://github.com/writer/framework-tutorials/tree/main/release-notes-generator/end) or in `framework-tutorials/release-notes-generator/end` in the `tutorials` repo you cloned at the beginning of the tutorial.

## Conclusion

By following these steps, you've created a complete Release notes generator application using Writer Framework. To learn more, explore the rest of the Writer Framework documentation and the API documentation.


# Repeater
Source: https://dev.writer.com/framework/repeater



The *Repeater* component allows you to repeat a group of components according to a list or dictionary.

## How it works

*Repeater* repeats its contents for every item of a given list or dictionary. It's similar to a `for each` construct.

Each iteration is rendered with a different **context**, a dictionary containing the key and value for the relevant item. By default, in the variables `itemId` and `item`.

### Food Selector example

<img alt="Repeater example" />

Given the state below, the contents of *Repeater* will be repeated 3 times. For the first iteration, `itemId` will equal `Banana`, and `item` will equal `{"type": "fruit", "colour": "yellow"}`. Components inside *Repeater* will be able to access this data using references such as `@{itemId}` and `@{item.type}`.

```py theme={null}
wf.init_state({
    "articles": {
        "Banana": {
            "type": "fruit",
            "colour": "yellow"
        },
        "Lettuce": {
            "type": "vegetable",
            "colour": "green"
        },
        "Spinach": {
            "type": "vegetable",
            "colour": "green"
        }
    },
    "order_list": []
})
```

## Event context

When working with *Repeater* components, you can get the context data using the `context` argument in the event handler.

Continuing with the Food Selector example above, the following handler can be linked to a *Button* —allowing users to add items to a list.

```py theme={null}
# The event handler below adds the itemId
# of the relevant article to the order list
def handle_add_to_list(state, context):
    state["order_list"] += [context["itemId"]]
```

<Warning>
  Binding directly to context isn't possible

  Use dynamic accessors when binding inside a *Repeater*. For example, `articles[itemId].colour`. This will bind to a different property of `articles` for every `itemId`.
</Warning>


# Sample app library
Source: https://dev.writer.com/framework/sample-apps



Writer Framework lets you build a wide variety of applications across different domains. Here are some sample apps to get you started, which are all [available on GitHub](https://github.com/writer/framework-tutorials/).

<Card title="Code generator" icon="code" href="https://github.com/writer/framework-tutorials/tree/main/code-generator">
  Generate React apps editable in a code sandbox.
</Card>

<Card title="Finance content checker" icon="file-invoice-dollar" href="https://github.com/writer/framework-tutorials/tree/main/finance-content-checker">
  Tool for analyzing financial content based on compliance guidelines.
</Card>

<Card title="Finance dashboard" icon="chart-line" href="https://github.com/writer/framework-tutorials/tree/main/finance-dashboard">
  AI-powered interactive dashboard for summarizing and visualizing financial data and metrics. Check out the [app tour](https://writer.com/engineering/financial-app-writer-framework-palmyra-fin/) on our Engineering blog.
</Card>

<Card title="Localized promo dashboard" icon="globe" href="https://github.com/writer/framework-tutorials/tree/main/localized-promo-dashboard">
  Tool to generate promotional content based on uploaded demographic data.
</Card>

<Card title="Patient portal" icon="hospital-user" href="https://github.com/writer/framework-tutorials/tree/main/patient-portal">
  Generates SOAP notes and extracts ICD codes from patient-doctor chat.
</Card>

<Card title="Product description embedded chat" icon="comments" href="https://github.com/writer/framework-tutorials/tree/main/pd-embedded-chat">
  Use an embedded no-code chat app to chat with a Knowledge Graph about products.
</Card>

<Card title="Product description page generator" icon="file-lines" href="https://github.com/writer/framework-tutorials/tree/main/pdp-generator">
  Tool for generating formatted product description pages.
</Card>

<Card title="Release notes generator" icon="clipboard-list" href="https://github.com/writer/framework-tutorials/tree/main/release-notes-generator">
  Utility for creating formatted, downloadable release notes from CSVs containing GitLab notes. [Full tutorial here](./release-notes-generator).
</Card>


# SEO and social sharing
Source: https://dev.writer.com/framework/seo



Writer Framework provides powerful options for optimizing your application's metadata to improve SEO (Search Engine Optimization) and enhance how your content appears when shared on social networks. These options are available in the `writer.serve.configure_webpage_metadata` function available in the `server_setup.py` file.

## Page title configuration

The page title is a crucial element for your application's SEO. Web crawlers and bots will see this title without loading the full application. By default, they will see `Writer Framework`.

```python theme={null}
# ./server_setup.py
writer.serve.configure_webpage_metadata(title="My Amazing App")
```

For dynamic titles, you can use a function instead of a static string. The title will be evaluated when a bot loads the page:

```python theme={null}
# ./server_setup.py
def _title():
	last_news = db.get_last_news()
	return f"Last news: {last_news.title}"

writer.serve.configure_webpage_metadata(title=_title)
```

## Meta tags configuration

HTTP headers allow you to specify a title, description, and keywords that search engines will use to understand and index your content. You can configure these tags using the `meta` parameter.

```python theme={null}
# ./server_setup.py
writer.serve.configure_webpage_metadata(
	title="My Amazing App",
    meta={
    	"description": "My amazing app",
    	"keywords": "WF, Amazing, AI App",
    	"author": "Amazing Company"
    }
)
```

You can also generate meta tags dynamically using a function:

```python theme={null}
# ./server_setup.py
def _meta():
	last_news = db.get_last_news()
	return {
		"description": f"Last news: {last_news.title}",
		"keywords": f"{last_news.keywords}",
		"author": "Amazing Company"
	}

writer.serve.configure_webpage_metadata(meta=_meta)
```

## Social network configuration

When users share links to your application on social networks, those platforms will attempt to fetch metadata to create rich previews of your content. You can configure the OpenGraph tags to improve the appearance of your content when shared.

```python theme={null}
# ./server_setup.py
writer.serve.configure_webpage_metadata(
    opengraph_tags= {
    	"og:title": "My App",
    	"og:description": "My amazing app",
    	"og:image": "https://myapp.com/logo.png",
    	"og:url": "https://myapp.com"
    }
)
```

Like meta tags, OpenGraph tags can also be generated dynamically:

```python theme={null}
# ./server_setup.py
def _opengraph_tags():
	last_news = db.get_last_news()
	return {
		"og:title": f"Last news: {last_news.title}",
		"og:description": f"{last_news.description}",
		"og:image": f"{last_news.image}",
		"og:url": f"https://myapp.com/news/{last_news.id}"
	}

writer.serve.configure_webpage_metadata(opengraph_tags=_opengraph_tags)
```

By configuring your application's metadata, your application will be more visible in search engines and social networks.


# Sessions
Source: https://dev.writer.com/framework/sessions



Sessions are designed for advanced use cases, being most relevant when Framework is deployed behind a proxy.

## Session information in event handlers

You can access the session's unique id, HTTP headers and cookies from event handlers via the `session` argument —similarly to how `state` and `payload` are accessed. The data made available here is captured in the HTTP request that initialised the session.

The `session` argument will contain a dictionary with the following keys: `id`, `cookies` and `headers`. Values for the last two are themselves dictionaries.

```py theme={null}
# The following will output a dictionary
# with the session's id, cookies and headers.
def session_inspector(session):
    print(repr(session))
```

This enables you to adapt the logic of an event to a number of factors, such as the authenticated user's role, preferred language, etc.

## Session verifiers

You can use session verifiers to accept or deny a session based on headers or cookies, thus making sure that users without the right privileges don't get access to the initial state or components.

Session verifiers are functions decorated with `wf.session_verifier` and are run every time a user requests a session. A `True` value means that the session must be accepted, a `False` value means that the session must be rejected.

```py theme={null}
import writer as wf

# Users without the header x-success will be denied the session

@wf.session_verifier
def check_headers(headers):
    if headers.get("x-success") is None:
        return False
    return True
```


# Social post generator
Source: https://dev.writer.com/framework/social-post-generator



In this tutorial, you'll use the Writer Framework to build an AI-powered tool for generating social media posts and tags based on the input you provide!

The process will take only minutes using a drag-and-drop visual editor to build the user interface and Python for the back-end code.

Here's what the finished project will look like:

<img alt="Finished social post generator project" />

## Prerequisites

Before starting, ensure you have:

* **A Writer account:** You don't need an account to use Writer Framework, but you'll need one to use the AI module. [Sign up for a free account here](https://app.writer.com/register).
* **Python 3.9.2 or later**: Use the installer from [python.org](https://www.python.org/downloads/).
* **pip:** This command-line application comes with Python and is used for installing Python packages, including those from Writer.
* **A basic understanding of Python:** You should be familiar with the basics of the language.
* **Your favorite code editor (optional):** There's a code editor built into Writer for writing back-end code, but you can also use Visual Studio Code, Notepad++, Vim, Emacs, or any text editor made for programming if you prefer.

## Setting up your project

### Create a Writer app and get its API key

First, you'll need to create a new app within Writer.

<Steps>
  <Step title="Create the app in Writer">
    Log into Writer. From the Home screen, click on the **Build an app** button.

    <img alt="Writer home screen" />

    The **Start building** menu will appear, presenting options for the types of apps you can create.

    Select **Framework**, located under **Developer tools**. This will create a brand new app based on Writer Framework.

    <img alt="&#x22;Start building&#x22; menu" />
  </Step>

  <Step title="Copy your app's API key">
    On the next screen, titled **How to deploy an application**, you can get the API key for the app by clicking on the **Reveal key** button, located under the text **Authenticate with an API key**. Your complete API key will be displayed, and a "copy" button will appear. Click this button to copy the key; you'll use it in the next step.

    <img alt="&#x22;How to deploy an application&#x22; page" />
  </Step>
</Steps>

### Set up your computer and create the app's project

The next step is to set up the Writer Framework environment on your computer. You'll do this by creating a directory for the project, installing dependencies, and creating the project for the application using a template.

<Steps>
  <Step title="Open your terminal application">
    Open your terminal application. On macOS and Linux, this application goes by the name *Terminal*; on Windows, you can use either *Windows PowerShell* (which is preferred) or *Command Prompt*.
  </Step>

  <Step title="Install the dependencies">
    <Note>If you already have the `writer` and `python-dotenv` packages installed on your computer, you can skip this step.</Note>

    Install the `writer` and `python-dotenv` packages by entering the following commands in your terminal application:

    ```
    pip install writer python-dotenv
    ```

    This command tells `pip`, the Python package installer, to install two packages:

    * `writer`, which provides some command-line commands and enables Python code to interact with Writer and the Writer Framework.
    * `python-dotenv`, which makes it easy to manage environment variables by loading them from a `.env` file. This one is optional for this exercise, but you might find it useful when working on larger projects.
  </Step>

  <Step title="Set the API key environment variable">
    To pass your API key to the Writer Framework, you need to set an environment variable called `WRITER_API_KEY`.

    Select your operating system and terminal application below, then copy and paste the command into your terminal application, replacing `[your_api_key]` with the API key you copied earlier:

    <CodeGroup>
      ```sh macOS/Linux (Terminal) theme={null}
      export WRITER_API_KEY=[your_api_key]
      ```

      ```sh On Windows (Windows PowerShell) theme={null}
      $env:WRITER_API_KEY=[your_api_key]
      ```

      ```sh On Windows (Command Prompt) theme={null}
      set WRITER_API_KEY=[your_api_key]
      ```
    </CodeGroup>

    The `WRITER_API_KEY` environment variable will remain defined as long your terminal session is open (that is, until you close your terminal application’s window).
  </Step>

  <Step title="Create the project">
    Create the project by entering this command into your terminal application:

    ```
    writer create social-post-generator --template=ai-starter
    ```

    This command sets up a new project called `social-post-generator` using a starter template called `ai-starter` so that you're not starting "from scratch."
  </Step>
</Steps>

## Build the UI

Now that you've created the project, it's time to define the UI. The Writer Framework's drag-and-drop capabilities make it easy — even if you haven't done much UI work before!

The project editor is a web application that runs on your computer and enables you to define and edit your app's user interface. Launch it by typing the following into your terminal application:

```
writer edit social-post-generator
```

You'll see a URL. Control-click it (command-click on macOS) to open it, or copy the URL and paste it into the address bar of a browser window.

The browser window will contain the project editor, which will look like this:

<img alt="Project editor" />

You'll see the following:

* The **canvas** is in the center. It displays the app's user interface.
* The column on the left contains:
  * The **Core toolkit**, which contains all the UI components. You define the user interface by dragging components from the Toolkit and placing them on the canvas.
  * The **Component tree**, which shows the arrangement of the UI components on the canvas. It's also useful for selecting items on the canvas, especially when it has a lot of UI components.

It's time to build the UI!

<Steps>
  <Step title="Examine the header">
    Select the **Header** component by clicking it — it's the component at the top, containing the title **AI STARTER** and a gray area labeled **Empty Header**.

    When you click it, you'll see the **properties** panel appear on the right side of the page. This lets you view and edit the properties of the selected component.

    <img alt="The selected header and its properties panel" />

    The first property you'll see in the panel is the **Text** property, which defines the text that appears as the header's title. It should contain the value `@{my_app.title}`. The `@{` and `}` indicate that `my_app.title` is a variable and that its contents should be the text displayed instead of the literal text "my\_app.title". You'll set the value of this variable soon.
  </Step>

  <Step title="Clear the Section's default title">
    Select the **Section** component by clicking it — it's just below the **Header** component and contains the title **Section Title** and a gray area labeled **Empty Section**.

    In the **properties** panel, clear out the value of the **Title** property. This will remove the *Section*'s default title.

    <img alt="The selected section and its properties panel" />
  </Step>

  <Step title="Add a Text Input component">
    The user will need a place to enter words or phrases that the app will use as the basis for generating posts and tags.

    Drag a **Text Input** component — and note, it's **Text *Input***, not **Text** —  from the **Core toolkit** panel on the left (it's under **Input**, and you may need to scroll down a little to find it) and into the **Section**. Sections can act as containers for other components.

    <Note>You can search for a specific component by using the search bar at the top of the **Core toolkit** panel.</Note>

    Select the **Text Input** component. In the **properties** panel:

    * Find the **Label** property and set its value to `Topic for social posts and tags`.
    * (Optional) Feel free to add some placeholder to the *Text Input* component by setting the value of the **Placeholder** property with something like `Enter words or phrases describing your topic`.

      <img alt="The text input component and its properties panel" />
  </Step>

  <Step title="Add a Button component">
    Drag a **Button** component from the **Core toolkit** panel (it's under **Other**, and you may need to scroll down a little to find it) into the **Section**, directly below the **Text Input**. The user will click this button to submit their prompt.

    Select the **Button**. In the **properties** panel:

    * Set the **Text** property's value to `Generate posts`.
    * Find the **Icon** property, and set its value to `arrow_forward`.

      <img alt="The button component and its properties panel" />
  </Step>

  <Step title="Add a Message component">
    The process of creating social media posts and tags takes a few moments. In order to reassure the user that the app is working and hasn't crashed, it will use a **Message** component to display something reassuring while it's generating.

    Drag a **Message** component from the **Core toolkit** panel into the **Section** positioning it immediately below the **Button**.

    Select the **Message** component. In the **properties** panel:

    * Scroll down to the **Style** section and look for the **Loading** property, which sets the color of the **Message** component when it's loading.
    * Click its **CSS** button, which will cause a text field to appear below it.
    * Enter this color value into the text field: `#D4FFF2`.

      <img alt="The message component and its properties panel" />
  </Step>

  <Step title="Add a new Section">
    The **Section** that you were working on is for user input. Let's add a new **Section** to hold the output — the social media posts and tags the app will generate.

    Drag a **Section** component from the **Toolbox** panel and place it *inside* the **Section** that's already there, just below the **Message** component.

    <Note>That's right — **Sections** can contain other **Sections**!</Note>

    <img alt="The new section inside the existing section" />

    Select the **Section** you just added. In the **properties** panel:

    * Find the **Title** property and clear it its value to remove the **Section**'s title.
    * Scroll down to the **Style** section and look for the **Container background** property, which sets the **Section**'s background color.
    * Click its **CSS** button, which will cause a text field to appear below it.
    * Enter this color value into the text field: `#F6EFFD`.

      <img alt="The new section and its properties" />
  </Step>

  <Step title="Add a Tags component">
    Writer Framework has a number of useful components to make your apps more functional and beautiful. One of these is the **Tags** component, which can take a list of hashtags (or words, or short phrases) and display them inside colorful "bubbles" to make them stand out. This app will display the social media tags it generates in a **Tags** component.

    Drag a **Tags** component from the **Toolbox** panel and place it inside the new **Section**.

    <img alt="The tags component" />
  </Step>

  <Step title="Add a Separator">
    Drag a **Separator** component from the **Toolbox** panel and place it inside the new **Section**, just below the **Tags** component. This will separate the tags from the posts.

    <img alt="The separator" />
  </Step>

  <Step title="Add a Text component">
    Finally, drag a **Text** component from the **Toolbox** panel and position it below the **Separator**. This will hold the generated social media posts.

    <img alt="The text component" />
  </Step>
</Steps>

## Add the back-end code

With the UI laid out, it's time to work on the logic behind it.

The logic behind the user interface is defined in a file named `main.py`, which is in your project's directory. This file was automatically generated; you'll update the code in it to define the behavior of your app.

The simplest way to edit `main.py` is within the project editor. Click on the "toggle code" button (beside the word **Code**) near the lower left corner of the project editor page.

<img alt="Project editor with arrow pointing to toggle code button" />

A pane with the name **Code** will appear at the bottom half of the screen, displaying an editor for the the contents of `main.py`.

<img alt="Project editor with the code editor displayed" />

<Note>If you'd rather use a code editor instead of coding in the browser, use it to open the `main.py` file in your project's directory.</Note>

Now follow these steps:

<Steps>
  <Step title="Import libraries and load the Writer Framework API key">
    You should see the following at the start of the file:

    ```python theme={null}
    import writer as wf
    import writer.ai
    ```

    Replace that code with the following:

    ```python theme={null}
    import os
    import re
    import writer as wf
    import writer.ai

    # Set the API key
    wf.api_key = os.getenv("WRITER_API_KEY")
    ```

    This code imports the libraries that the application will need and then reads your Writer Framework API key in the `WRITER_API_KEY` environment variable.
  </Step>

  <Step title="Create a handler to respond to the user's input">
    When the user presses the app's **Button**, the app needs to call a function to generate and display the social media posts and tags. Find these comments in the code...

    ```python theme={null}
    # Welcome to Writer Framework! 
    # This template is a starting point for your AI apps.
    # More documentation is available at https://dev.writer.com/framework
    ```

    ...and replace them with the following function:

    ```python theme={null}
    def generate_and_display_posts_and_tags(state):
        print(f"Here's what the user entered: {state['topic']}")

        # Display message
        state["message"] = "% Generating social media posts and tags for you..."

        # Generate and display social posts
        prompt = f"You are a social media expert. Generate 5 engaging social media posts about {state['topic']}. Include emojis, and put a blank line between each post."
        state["posts"] = writer.ai.complete(prompt)
        print(f"Posts: {state['posts']}")

        # Generate and display hashtags
        prompt = f"You are a social media expert. Generate around 5 hashtags about {state['topic']}, delimited by spaces. For example, #dogs #cats #ducks #elephants #badgers"
        pattern = r"#\w+"
        hashtags = re.findall(pattern, writer.ai.complete(prompt))
        state["tags"] = {item: item for item in hashtags}
        print(f"Tags: {state['tags']}")

        # Hide message
        state["message"] = ""
    ```

    The `%` at the start of the string being assigned to `state["message"]` will be replaced by a “spinning circle” progress indicator graphic in the *Message* component.

    The `pattern` variable in the `# Generate and display hashtags` section defines a regular expression pattern to search for words that begin with the `#` character. The `r` in front of the opening quote specifies that the string is a *raw string*, which means that the `\` character should be treated as a literal backslash and not as the start of an escape character sequence.

    Note that `generate_and_display_posts_and_tags()` uses `print()` functions for debugging purposes, and you can use them to get a better idea of what's happening in the function. You'll see their output in both your terminal application and in the project editor's 'log' pane (which will be covered shortly) as you use the social post generator. This output will include:

    * The topic the user entered
    * The posts generated by the LLM
    * The hashtags generated by the LLM

    The `print()` functions don't affect the operation of the social post generator in any way, and you can remove them if you wish.
  </Step>

  <Step title="Initialize the application">
    The final step is to set the application's initial state. Find this code, which should be just after the `generate_and_display_posts_and_tags()` function...

    ```python theme={null}
    # Initialise the state
    wf.init_state({
        "my_app": {
            "title": "AI STARTER"
        },
    })
    ```

    ...and replace it with this:

    ```python theme={null}
    # Initialize the state
    wf.init_state({
        "topic": "writing",
        "message": "",
        "tags": {},
        "posts": "",
        "my_app": {
            "title": "SOCIAL POST GENERATOR"
        }
    })
    ```

    The Writer Framework's `init_state()` method sets the initial value of `state`, a dictionary containing values that define the state of the application. The key-value pairs in `state` are how you store values used by your app and how you pass data between the back-end code and the UI.

    The code above sets the initial value of `state` so that it has these key-value pairs:

    * `topic`: A string containing the topic that the application should generate social media posts and tags for. You'll bind its value to the *Text Input* component where the user will enter the topic.
    * `message`: A string containing text of the message that will be displayed to the user while the application is generating posts and tags. You'll bind its value to the **Message** component.
    * `tags`: A list containing the hashtags generated by the LLM. You'll bind its value to the **Tags** component.
    * `posts`: A string containing the social media posts generated by the LLM. You'll bind its value to the **Text** component.
    * `my_app`: A dictionary containing values that define the application's appearance. This dictionary has a single key-value pair, `title`, which defines the text that appears as the application's title.

    <Note>For more details about the `state` variable, see our [*Application state*](https://dev.writer.com/framework/application-state#application-state) page.</Note>
  </Step>

  <Step title="Save the updated code and hide the code editor">
    That’s all the code. If you edited the code in the browser, save it by clicking the “save” button near the top right corner of the code editor.

    <img alt="Project editor and code editor, with arrow pointing to save button" />

    Click the "toggle code" button to hide the code editor.
  </Step>
</Steps>

## Bind the UI to the back-end code

You've built the UI and written the code behind it. Let's connect the two! Go back to the browser window with the project editor and do the following:

<Steps>
  <Step title="Observe that the heading at the top of the app is now 'SOCIAL POST GENERATOR'">
    Earlier, you saw that the **Header** component's **Text** property was set to `@{my_app.title}`, a value in the app's `state` variable. You changed this value when you update the call to the Writer Framework's `init_state()` method.
  </Step>

  <Step title="Bind the Text Input component to the 'state' variable's 'topic' key">
    Select the **Text Input** component. In the **properties** panel, scroll down to the **Binding** section and find the **State element** property. This is where you specify the `state` variable key whose value will be connected to the **Text Input** component. Set its value to `topic`.

    <img alt="Updating the text input component's state element property" />
  </Step>

  <Step title="Connect the Button component to the 'generate_and_display_posts_and_tags()' function">
    Select the **Button** component. In the **properties** panel, scroll down to the **Events** section and find the **`wf-click`** property. This is where you specify the function to call when the user clicks the button — set its value to `generate_and_display_posts_and_tags`.

    <img alt="Updating the button's wf-click property" />
  </Step>

  <Step title="Bind the Message component to the 'state' variable's 'message' key">
    Select the **Message** component. In the **properties** panel, find the **Message** property, which specifies the content of the **Message** component. Set its value to `@{message}`.

    <img alt="Updating the message's message property" />
  </Step>

  <Step title="Bind the Tags component to the 'state' variable's 'tags' key.">
    Select the **Tags** component. In the **properties** panel:

    * Find the **Tags** property, which specifies the source of the tags that the component will display.
    * Click its **JSON** button.
    * In the text field below the **JSON** button, set the **Tags** property's value to `@{tags}`.

      <img alt="Updating the tags component's tags property" />
  </Step>

  <Step title="Bind the Text component to the 'state' variable's 'posts' key">
    Select the **Text** component. In the **properties** panel:

    * Find the **Text** property, which specifies the content of the **Text** component. Set its value to `@{posts}`.
    * Set the **Use Markdown** property to **yes**.

      <img alt="Updating the text component's properties" />
  </Step>

  <Step title="Set the visiblity of the Section component containing the Tags and Text components based on the 'state' variable's 'posts' key">
    Select the **Section** component containing the **Tags** and **Text** components. In the **properties** panel:

    * Scroll to the **Visibility** property at the bottom.
    * Click on the **Custom** button.
    * In the **Visibility value** field, set the value to `posts`. This will cause the **Section** to be visible only when the `state` variable's `posts` key has a non-empty value.

      <img alt="Updating the inner section's visibility property" />
  </Step>
</Steps>

## Test the application

You've completed all the steps to make a working social post generator, and you can try using it right now, even while editing the user interface!

Enter a topic into the **Topic for social posts and tags** text field, then click the **Generate Posts** button\* *twice* — the first time will cause the **properties** panel to appear, and the second click will register as a click. You'll know that you've clicked the button when you see the **Message** component display the text “Generating social media posts and tags for you...”

<img alt="Waiting for the generator to finish while the message component displays its message" />

...and soon after that, you should see some results:

<img alt="The results" />

To get a better sense of what the experience will be like for the user, switch to the preview by changing the edit mode (located near the upper left corner of the page) from *UI* mode to *Preview* mode by selecting the **Preview** option:

<img alt="The project editor with an arrow pointing to the Preview button" />

Here’s what the app looks like in *Preview* mode:

<img alt="The working social post generator, with the project editor in &#x22;Preview&#x22; mode" />

You can see the output of any `print()` functions and error messages by clicking on the **Log** button located near the upper right corner of the page:

<img alt="The social post generator with an arrow pointing to the Log button" />

Here’s what the app looks like when displaying the log:

<img alt="The social post generator, with the log pane displayed" />

It's very helpful to be able to test the application while editing it. As you continue to work with Writer Framework, you'll find yourself alternating between making changes to your application and testing those changes without having to leave the project editor.

## Run the application locally

Once you've tested the application, it's time to run it locally.

Switch back to your terminal application. Stop the editor with ctrl-c, then run the application by entering the following command:

```
writer run social-post-generator
```

Note that the command starts with `writer run` as opposed to `writer edit`. This launches the application as your users will see it, without any of the editing tools. Even though you can preview your applications in the project editor, it's still a good idea to test it by running it on your computer, outside the project editor, before deploying it.

You'll be able to access the application with your browser at the URL that appears on the command line. It should look like this:

<img alt="Finished social post generator project" />

<Note>The Writer editor, which you launched with `writer edit social-post-generator`, and your application, which you launched with `writer run social-post-generator`, run  on the same URL, but on different *ports* (specified by the number after the `:` character at the end of the URL).</Note>

## Conclusion

That's it — you've built a functional social post generator using the Writer Framework!

Feel free to modify this project! The Writer platform is flexible enough for you to customize, extend, and evolve your application into something completely different! To find out what else you can do, check out the documentation for [Writer Framework](https://dev.writer.com/framework/introduction) and the [Writer API](https://dev.writer.com/introduction).


# State schema
Source: https://dev.writer.com/framework/state-schema



Schema declarations on the [Application state](./application-state) allows Framework to handle complex serialisation
scenarios, while also allowing your IDE and toolchains to provide autocomplete and type checking.

## Schema declaration

```python theme={null}
import writer as wf

class AppSchema(wf.WriterState):
    counter: int

initial_state = wf.init_state({
    "counter": 0
}, schema=AppSchema)

# Event handler
# It receives the session state as an argument and mutates it
def increment(state: AppSchema):
    state.counter += 1
```

Accessing an attribute by its key is always possible.

```python theme={null}
def increment(state: AppSchema):
    state["counter"] += 1
```

Attributes missing from the schema remain accessible by their key.

```python theme={null}
initial_state = wf.init_state({
    "counter": 0,
    "message": None
}, schema=AppSchema)

def increment(state: AppSchema):
    state['message'] = "Hello pigeon"
```

## Schema composition

Schema composition allows you to model a complex Application state.

```python theme={null}
class MyappSchema(wf.State):
    title: str

class AppSchema(wf.WriterState):
    my_app: MyappSchema
    counter: int

initial_state = wf.init_state({
    "counter": 0,
    "my_app": {
        "title": "Nested value"
    }
}, schema=AppSchema)
```

## Calculated properties

Calculated properties are updated automatically when a dependency changes.
They can be used to calculate values derived from application state.

```python theme={null}
class MyAppState(wf.State):
  counter: List[int]

class MyState(wf.WriterState):
  counter: List[int]

  @wf.property(['counter', 'app.counter'])
  def total_counter(self):
    return sum(self.counter) + sum(self.app.counter)

initial_state = wf.init_state({
    "counter": 0,
    "my_app": {
        "counter": 0
    }
}, schema=MyState)
```

## Multi-level dictionary

Some components like *Vega Lite Chart* require specifying a graph in the form of a multi-level dictionary.

A schema allows you to specify that an attribute which contains a dictionary
must be treated as a dictionary, rather than as a group of state.

```python theme={null}
class AppSchema(wf.WriterState):
    vegas_graph: dict

# Without schema, this handler is execute only once
def handle_vega_graph(state: AppSchema):
    graph = state.vega_graph
    graph["data"]["values"][0]["b"] += 1000
    state.vega_graph = graph
    
initial_state = wf.init_state({
    "vegas_graph": {
        "data": {
            "values": [
                {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4},
                {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6},
                {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7}
            ]
        },
        "mark": "bar",
        "encoding": {
            "x": {"field": "a", "type": "nominal"},
            "y": {"aggregate": "average", "field": "b", "type": "quantitative"}
        }
    },
}, schema=AppSchema)
```

## Type checking

A schema allows you to check the integrity of your back-end using the type system.
The code below will raise an error with mypy.

```bash theme={null}
$ mypy apps/myapp/main.py
apps/myapp/main.py:7: error: "AppSchema" has no attribute "countr"; maybe "counter"?  [attr-defined] 
```

Here is the code, can you spot the error ?

```python theme={null}
import writer as wf

class AppSchema(wf.WriterState):
    counter: int

def increment(state: AppSchema):
    state.countr += 1

initial_state = wf.init_state({
    "counter": 26,
}, schema=AppSchema)
```


# Stylesheets
Source: https://dev.writer.com/framework/stylesheets



The appearance of your application can be fully customised via CSS stylesheets. These are dynamically linked during runtime and can be switched from the back-end, targeting all or specific sessions.

## Importing a stylesheet

Stylesheet imports are triggered via Framework's `mail`, similarly to other features discussed in [Backend-initiated actions](/framework/backend-initiated-actions). When the import is triggered, the front-end downloads the specified stylesheet and creates a `style` element with its contents.

The `import_stylesheet` method takes the `stylesheet_key` and `path` arguments. The first works as an identifier that will let you override the stylesheet later if needed. The second is the path to the CSS file.The path specified needs to be available to the front-end, so storing it in the `/static` folder of your app is recommended.

The following code imports a stylesheet when handling an event.

```py theme={null}
def handle_click(state):
    state.import_stylesheet("theme", "/static/custom.css")
```

In many cases, you'll want to import a stylesheet during initialisation time, for all users. This is easily achievable via the initial state, as shown below.

```py theme={null}
initial_state = wf.init_state({
    "counter": 1
})

initial_state.import_stylesheet("theme", "/static/custom.css")
```

<Tip>
  Use versions to avoid caching. During development time, stylesheets may be cached by your browser, preventing updates from being reflected. Append a querystring to bust the cache, e.g. use `/static/custom.css?3`.
</Tip>

## Applying CSS classes

You can use the property *Custom CSS classes* in the Builder's *Component Settings* to apply classes to a component, separated by spaces. Internally, this will apply the classes to the root HTML element of the rendered component.

<img alt="Stylesheets - Component Settings" />

## Tips for effective stylesheets

The CSS code for the class used earlier, `bubblegum`, can be found below. Note how the `!important` flag is used when targetting style attributes that are configurable via the Builder. If the flag isn't included, these declarations will not work, because built-in Framework styling is of higher specificity.

```css theme={null}
.bubblegum {
    background: #ff63ca !important;
    line-height: 1.5;
    transform: rotate(-5deg);
}

/* Targeting an element inside the component root element */
.bubblegum > h2 {
    color: #f9ff94 !important;
}
```

<Warning>
  Component structure may change. When targeting specific HTML elements inside components, take into account that the internal structure of components may change across Framework versions.
</Warning>

Alternatively, you can override Framework's style variables. This behaves slightly differently though; style variables are inherited by children components. For example, if a *Section* has been assigned the `bubblegum` class, its children will also have a pink background by default.

```css theme={null}
.bubblegum {
    --containerBackgroundColor: #ff63ca;
    --primaryTextColor: #f9ff94;
    line-height: 1.5;	
    transform: rotate(-5deg);
}
```

The class can be used in *Component Settings*. If the stylesheet is imported, the effect will be immediate. In case the stylesheet has been modified since imported, it'll need to be imported again.

<img alt="Stylesheets - Applied Classes" />

## Targeting component types

Framework components have root HTML elements with a class linked to their type. For example, *Dataframe* components use the class *CoreDataframe*. When writing a stylesheet, you can target all *Dataframe* components as shown below.

```css theme={null}
.CoreDataframe {
    line-height: 2.0;
}
```

## Implementing themes

It's possible to switch stylesheets during runtime, by specifying the same `stylesheet_key` in subsequent calls. This allows you to implement a "theme" logic if desired, targeting the whole or a specific part of your app.

```py theme={null}
def handle_cyberpunk(state):
    state.import_stylesheet("theme", "/static/cyberpunk_theme.css")

def handle_minimalist(state):
    state.import_stylesheet("theme", "/static/minimalist_theme.css")
```


# Testing
Source: https://dev.writer.com/framework/testing



Testing a Framework application is easy. Given that event handlers are plain Python functions that take arguments such as `state` and `payload`, you can inject your own and test whether the outcome is correct. This section will use `pytest` examples.

## State

### Accessing the initial state

To get started, import your app's entry point, `main`. This will initialise state and make event handlers available. The initial state is available in the module, at `main.wf.initial_state` provided you imported `writer` as `wf`.

### Creating states

For testing purposes, you can create your own state using the `WriterState` class in `writer.core`. Pass a dictionary when constructing it.

```py theme={null}
from writer.core import WriterState

artificial_state = WriterState({
    "a": 3,
    "b": 6
})
```

## Example

The code of a Framework application basically consists of two things:

1. Initial state
2. Event handlers

It's straightforward to test both, as shown below.

### The app

```py theme={null}
import writer as wf

def handle_multiplication(state):
    state["n"] = state["a"]*state["b"]

wf.init_state({
    "counter": 0,
    "a": 0,
    "b": 0
})
```

### The tests

```py theme={null}
from writer.core import WriterState
import main


class TestApp:

    initial_state = main.wf.initial_state
    artificial_state = WriterState({
        "a": 3,
        "b": 2
    })

    def test_counter_must_start_from_zero(self):
        assert self.initial_state["counter"] == 0

    def test_handle_multiplication(self):
        main.handle_multiplication(self.artificial_state)
        assert self.artificial_state["n"] == 6

```


# Account management
Source: https://dev.writer.com/home/account_management





# Agent Builder
Source: https://dev.writer.com/home/agent-builder-link





# Monitor agent performance
Source: https://dev.writer.com/home/agent-observability

Monitor agent performance with session logs and observability metrics in AI Studio. Track usage, analyze conversations, and debug agent behavior.

AI Studio provides session logs and observability metrics for agents so you can understand how your agents are performing.

The following observability views are available for individual agents:

* [Session logs](#session-logs)
* [Agent observability](#agent-observability)

You can also view [organization-wide usage and spend](/home/usage-and-spend).

## Session logs

Organization admins can choose to retain logs from agent sessions. When enabled, you can view the logs for an agent for the given retention period.

<Info>
  Session logs are only available to [organization
  admins](https://support.writer.com). Logs are available for custom [no-code
  agents](/no-code/introduction) and [Writer
  Agent](https://support.writer.com/article/235-ask-writer), but are not yet
  available for prebuilt agents.
</Info>

### Enable session logging

To enable session logging, log in to [AI Studio](https://app.writer.com/aistudio) and navigate to the **Admin** tab in the left sidebar.

<img alt="Session log page in the Admin tab, which shows a list of agents and their session log settings." />

From there, you can enable session logging for your organization.

Once enabled, you can choose the retention period for session logs for each agent. The default is no retention. The possible retention periods are 7 days, 30 days, 90 days, or 180 days.

Whenever someone updates an agent's session log settings, all organization admins receive an email notification.

### View session logs

To view session logs, navigate to [AI Studio](https://app.writer.com/aistudio) and select **Agents** from the left sidebar. Select an agent from the list, then select the **Observability** tab from the top of the page and choose the **Session logs** view. Click on an individual session to view the logs.

<img alt="Session logs tab in AI Studio" />

#### Flagged responses

In a chat with a no-code agent, an end user can flag a response as inappropriate or inaccurate. An organization admin can then view the flagged responses within the session logs.

To flag a response in a chat, the user clicks the **Flag** button in the bottom right corner of the response.

<img alt="Button to flag a response within a chat" />

In the session logs for a particular conversation, you can see any flagged responses by clicking the **View flagged responses** button in the top right corner of the session log.

<img alt="Button to view flagged responses for a particular conversation" />

## Agent observability

Agent observability provides a detailed view of an agent's engagement, performance, and usage. Anyone can view these metrics for agents in your organization. Session logs are also available, but [only for organization admins if the agent's session log settings are enabled](#view-session-logs).

These metrics are only available for deployed agents.

To view metrics for deployed agents, navigate to [AI Studio](https://app.writer.com/aistudio) and select **Agents** from the left sidebar. Select an agent from the list, then select the **Observability** tab from the top of the page and choose the specific metric you want to view.

* **Engagement metrics**: Show the top five users for the agent over the specified time period and the total number of user interactions for the agent.
* **Performance metrics**: Show the average response time as well as the P90, P95, and P99 response times for the agent over the specified time period. It also shows the response codes broken down by `200`, `400`, and `500` status codes.
* **Usage metrics**: Show the total number of tokens and the total spend for the agent over the specified time period.

## Next steps

* [Observability overview](/home/observability): Monitor events, errors, and performance across your deployment
* [Usage and spend](/home/usage-and-spend): Track organization-wide costs and token consumption
* [Export telemetry data](/home/integrations/openllmetry) to your existing observability tools via OpenLLMetry
* [Configure guardrails](/home/guardrails) to enforce content policies across your agents


# API Reference
Source: https://dev.writer.com/home/api-reference-link





# Invoke no-code agents via the API
Source: https://dev.writer.com/home/applications

Turn deployed no-code agents into microservices with the Applications API. Use text generation and research agents as tools in your applications.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>

The Applications API allows you to turn deployed [no-code agents](/no-code/introduction) into microservices, which can also be used as tools in [tool calling](/home/applications-tool-calling). Business users can define inputs, prompts, and outputs, and developers can then add them to other applications, UIs, or services.

This guide shows how to use the `/applications` endpoint to generate content from no-code agents.

If you do not have a deployed no-code agent, follow the guides to build an agent with [text generation](/no-code/text-generation) or [research](/no-code/research) capabilities in AI Studio.

<Warning>
  The `/applications` endpoint supports only agents with [text generation](/no-code/text-generation) and [research](/no-code/research) capabilities. It does not support [chat](/no-code/chat) agents.
</Warning>

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Endpoint overview

**URL:** `POST https://api.writer.com/v1/applications/{application_id}`

<Warning>
  Using the `/applications` endpoint incurs charges for **model usage**. See the [pricing page](/home/pricing) for more information.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.writer.com/v1/applications/<application-id>' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WRITER_API_KEY" \
  --data-raw '{
    "inputs": [
      {
        "id": "Product description",
        "value": [
          "Terra running shoe"
        ]
      }
    ]
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.applications.generate_content(
      application_id="<application-id>",
      inputs=[
          {
              "id": "Product description",
              "value": ["Terra running shoe"]
          }
      ]
  )

  print(response.suggestion)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.applications.generateContent(
      "<application-id>",
      {
          inputs: [
              {
                  id: "Product description",
                  value: ["Terra running shoe"]
              }
          ]
      }
  );

  console.log(response.suggestion);
  ```
</CodeGroup>

### Path parameters

| Parameter        | Type   | Description                                                                                                                                                                                                                                                                                                                                              |
| ---------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `application_id` | string | The ID of the deployed agent. You can find this in the URL of the agent in AI Studio: `https://app.writer.com/aistudio/organization/<organization-id>/applications/<application-id>`. You can also use the [List applications](/api-reference/application-api/list-applications) endpoint to get the list of deployed agents and see the application ID. |

### Request body

Below are the required and commonly used optional parameters for the `/applications` endpoint.

| Parameter        | Type           | Description                                                                                                                                                                                                                                                                                        |
| ---------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputs`         | array          | Input values matching the agent's defined input fields. The input structure depends on the agent capabilities.                                                                                                                                                                                     |
| `inputs[].id`    | string         | The name of the input field. For research, the input id is always `query`.                                                                                                                                                                                                                         |
| `inputs[].value` | array\[string] | An array containing values of the input field. You can pass multiple values for a single input field. If the input field is a file, the value should be the `file_id` from an uploaded file, such as `["1234-abcd-1234"]`. Learn more about how to upload files with the [Files API](/home/files). |

```json theme={null}
{
  "inputs": [
    {
      "id": "string",
      "value": ["string"]
    }
  ]
}
```

See the [applications endpoint reference](/api-reference/application-api/applications) for more information on the request body and the additional parameters you can use to control the agent.

### Response format

The response format depends on the agent capabilities and whether you set the `stream` parameter to `true` or `false`. Only agents with research support streaming responses at this time.

#### Non-streaming response

If you set the `stream` parameter to `false`, the response is delivered as a JSON object with the following parameters:

| Parameter    | Type   | Description                                                  |
| ------------ | ------ | ------------------------------------------------------------ |
| `title`      | string | The name of the output field, as defined in the application. |
| `suggestion` | string | The output of the application call.                          |

```json theme={null}
{
  "title": null,
  "suggestion": "The Terra running shoe combines lightweight materials with advanced cushioning technology to deliver exceptional comfort and performance for every stride."
}
```

#### Streaming response

If you set the `stream` parameter to `true`, the response is delivered as [server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events). The event contains several parameters.

The content of the chunk is in the `delta` field. You can also use the `stages` field to get the stages of the research assistant application call, such as "Planning the research" or "Researching and organizing findings."

Below are the important parameters for extracting the output of the application call.

| Parameter              | Type   | Description                                           |
| ---------------------- | ------ | ----------------------------------------------------- |
| `delta`                | object | The content of the chunk.                             |
| `delta.content`        | string | The output of the application call.                   |
| `delta.stages`         | array  | The stage of the research assistant application call. |
| `delta.stages.content` | string | The content of the stage.                             |

```json theme={null}
data: {'delta': {'title': None,
  'content': None,
  'stages': [{'id': 'a5d81590-0120-4404-948b-8075e3610f51',
    'content': 'Initiating a research assistant: 🚗 Automotive assistant',
    'sources': None}]}}
```

## Example: Text generation

Agents used for [text generation](/no-code/text-generation) generate content based on the inputs provided to a specific no-code agent created in AI Studio. This is a great way to enable business users to control prompts and outputs while still allowing developers to add them to other applications, UIs, or services.

To invoke text generation, you need to provide the application ID and the required inputs to the agent. Inputs are defined in the agent within AI Studio.

### Get the text generation inputs

To see the required inputs for the agent, you can view the agent in [AI Studio](https://app.writer.com/aistudio), or you can use the [Get application](/api-reference/application-api/application-details) endpoint to get the agent details, including the inputs.

The example below shows how to use the `Get application` endpoint to get the list of required inputs with their name and types.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request GET 'https://api.writer.com/v1/applications/<application-id>' \
   --header "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  application = client.applications.retrieve(
      "<application-id>",
  )

  for input_ in application.inputs:
    print(f"{input_.name}: {input_.input_type}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const application = await client.applications.retrieve("<application-id>");

  for (const input of application.inputs) {
    console.log(`${input.name}: ${input.input_type}`);
  }
  ```
</CodeGroup>

### Format the request body with the application inputs

The `inputs` parameter is an array of objects, each containing an `id` and `value` field. The `id` field is the name of the input field, and the `value` field is the value of the input field.

```json theme={null}
{
  "inputs": [
    {
      "id": "Text input name",
      "value": ["Input value 1"]
    },
    {
      "id": "File input name",
      "value": ["1234-fileid-1234"]
    }
  ]
}
```

If the input field is a file, the value should be the `file_id` from an [uploaded file](/api-reference/file-api/upload-files).

Your request body should be formatted as a JSON object with values for each input.

### Generate content from the agent

Once you've formatted the request body with the agent inputs, you can invoke the agent using the [Generate content](/api-reference/application-api/applications) endpoint.

The examples below demonstrate calling an agent with text generation capabilities. The agent takes two text inputs: `Product description` and `Target audience`.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST 'https://api.writer.com/v1/applications/<application-id>' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data-raw '{
    "inputs": [
      {
        "id": "Product description",
        "value": ["Terra running shoe"]
      },
      {
        "id": "Target audience",
        "value": ["Runners"]
      }
    ]
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.applications.generate_content(
      application_id="<application-id>",
      inputs=[
          {
              "id": "Product description",
              "value": ["Terra running shoe"]
          },
          {
              "id": "Target audience",
              "value": ["Runners"]
          }
      ]
  )

  print(response.suggestion)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.applications.generateContent(
      "<application-id>", 
      {
          inputs: [
              {
                  id: "Product description",
                  value: ["Terra running shoe"]
              },
              {
                  id: "Target audience",
                  value: ["Runners"]
              }
          ]
      }
  );

  console.log(response.suggestion);
  ```
</CodeGroup>

## Example: Research assistant

Agents with [research](/no-code/research) capabilities output a final report after performing research in stages. Because of this, they support streaming responses.

These agents take an array of inputs that contain a single item, formatted as a JSON object with `id` and `value` parameters. It also takes a `stream` parameter that is set to `false` by default.

| Parameter        | Type           | Description                                                                                       |
| ---------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| `inputs`         | array          | An array of objects, each containing an `id` and `value` field.                                   |
| `inputs[].id`    | string         | The name of the input field. For research assistant applications, the input id is always `query`. |
| `inputs[].value` | array\[string] | An array containing the research query.                                                           |
| `stream`         | Boolean        | Whether to stream the response. Defaults to `false`.                                              |

The examples below demonstrate calling an agent to research hotels in San Francisco. The application streams the output as it is generated.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST 'https://api.writer.com/v1/applications/<research-application-id>' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data-raw '{
    "inputs": [
      {
        "id": "query", 
        "value": ["Provide a list of three hotels in San Francisco near Union Square within the price range of $100 to $200 per night"]
      }
    ],
    "stream": true
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.applications.generate_content(
      application_id="<research-application-id>",
      inputs=[
          {
              "id": "query",
              "value": ["Provide a list of three hotels in San Francisco near Union Square within the price range of $100 to $200 per night"]
          }
      ],
      stream=True
  )

  for chunk in response:
      if chunk.delta.content:
          print(chunk.delta.content, end="", flush=True)
      elif chunk.delta.stages:
          print(chunk.delta.stages[0].content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.applications.generateContent(
      "<research-application-id>",
      { 
          inputs: [
              {
                  id: "query",
                  value: ["Provide a list of three hotels in San Francisco near Union Square within the price range of $100 to $200 per night"]
              }
          ],
          stream: true
      } 
  );

  for await (const chunk of response) {
      if (chunk.delta.content) {
          process.stdout.write(chunk.delta.content);
      } else if (chunk.delta.stages) {
          console.log(chunk.delta.stages[0].content);
      }
  }
  ```
</CodeGroup>

## Next steps

Now that you've invoked a no-code agent via the API, learn other ways you can use no-code agents via the API:

* [Run no-code agents asynchronously](/home/async-applications)
* [Use no-code agents in tool calling](/home/applications-tool-calling)


# Use no-code agents as tools
Source: https://dev.writer.com/home/applications-tool-calling



<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>

You can use deployed [no-code agents](/no-code/introduction) as tools in [chat completions](/api-reference/completion-api/chat-completion) with Palmyra X4 and X5.

This approach allows you to:

* Use no-code agents alongside other tools
* Chain multiple no-code agents together
* Combine outputs with other API calls or business logic

If you don't have a deployed no-code agent, build one with [text generation](/no-code/text-generation) or [research](/no-code/research) capabilities in AI Studio and [deploy it](/no-code/deploying-an-agent) to get an application ID.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Tool structure

To use a no-code agent as a tool, define the tool in a `tools` array when making the request to the [chat endpoint](/home/chat-completion).

The `tools` array contains an object with the following parameters:

| Parameter                        | Type   | Description                                                                                                                      |
| -------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `type`                           | string | The type of tool, which is `function` for a no-code agent                                                                        |
| `function`                       | object | An object containing the tool's description and application ID                                                                   |
| `function.name`                  | string | The name of the tool                                                                                                             |
| `function.description`           | string | A description of what the tool will be used for                                                                                  |
| `function.parameters`            | object | An object containing the tool's input parameters                                                                                 |
| `function.parameters.type`       | string | The string `object`                                                                                                              |
| `function.parameters.properties` | object | An object containing the tool's parameters in the form of a [JSON schema](https://json-schema.org/). See below for more details. |
| `function.parameters.required`   | array  | An array of the tool's required parameters                                                                                       |

### function.parameters.properties

The `function.parameters.properties` object contains the tool's parameter definitions as a [JSON schema](https://json-schema.org/). The object's keys should be the names of the parameters, and the values should be objects containing the parameter's type and description.

When the model decides you should use the tool to answer the user's question, it will return the parameters that you should use when calling the function you've defined.

Below is an example of a tool definition for a no-code agent that generates product descriptions.

<CodeGroup>
  ```python Python theme={null}
  tools = [
      {
          "type": "function",
          "function": {
              "name": "generate_product_description",
              "description": "A function that will invoke an agent for text-generation, specialized in generating product descriptions. Any user request asking for product descriptions should use this tool.",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "product_name": {
                          "type": "string",
                          "description": "The name of the product"
                      }
                  },
                  "required": ["product_name"]
              }
          }
      }
  ]
  ```

  ```javascript JavaScript theme={null}
  const tools = [
      {
          type: "function",
          function: {
              name: "generate_product_description",
              description: "A function that will invoke an AI agent for text-generation, specialized in generating product descriptions. Any user request asking for product descriptions should use this tool.",
              parameters: {
                  type: "object",
                  properties: {
                      product_name: {
                          type: "string",
                          description: "The name of the product"
                      }
                  },
                  required: ["product_name"]
              }
          }
      }
  ];
  ```
</CodeGroup>

<Note>
  To help the model understand when to use the tool, follow these best practices for the `function.description` parameter:

  * Indicate that the tool is a function that invokes a no-code agent
  * Specify the agent's purpose and capabilities
  * Describe when the tool should be used

  An example description for a tool that invokes an agent with text-generation:

  > "A function that invokes an AI agent for text-generation, specialized in generating product descriptions. Any user request asking for product descriptions should use this tool."
</Note>

### Response format

When the model decides to use a custom tool, the response from the tool call is returned in the `tool_calls` object. The `tool_calls` object contains the following fields:

| Parameter                          | Type   | Description                                                              |
| ---------------------------------- | ------ | ------------------------------------------------------------------------ |
| `tool_calls[0].id`                 | string | The ID of the tool call                                                  |
| `tool_calls[0].function`           | object | An object containing the function to call                                |
| `tool_calls[0].function.type`      | string | The type of tool, which is `function` for a no-code agent                |
| `tool_calls[0].function.name`      | string | The name of the function to call                                         |
| `tool_calls[0].function.arguments` | string | A JSON-formatted string containing the arguments to pass to the function |

Here is an example of a chat completion response that includes a tool call. In this example, the model decides to use the `generate_product_description` tool to answer the user's question, and provides the arguments to pass to the function.

```json sample response[expandable] {11} theme={null}
{
"content": null,
"refusal": null,
"role": "assistant",
"graph_data": {
"sources": null,
"status": null,
"subqueries": null
},
"llm_data": null,
"tool_calls": [
{
    "id": "chatcmpl-tool-123",
    "function": {
    "arguments": "{\"product_name\": \"Terra running shoe\"}",
    "name": "generate_product_description"
    },
    "type": "function",
    "index": null
}
],
"image_data": null
}
```

## Usage example

The following example demonstrates how to use a no-code agent that performs text generation tasks as a tool in a chat completion. The example uses a hypothetical product description agent, but you can use any no-code agent in this way.

### Create a function that calls your deployed agent

First, create a function that calls your deployed agent. You will use this function if the LLM you're chatting with decides to use the tool.

The function below takes a `product_name` parameter, invokes the no-code agent with the agent's required input fields, and returns the generated product description.

If you are unfamiliar with how to invoke a no-code agent with the SDK, see the [no-code agent guide](/home/applications).

<CodeGroup>
  ```python Python theme={null}
  def generate_product_description(product_name):
      response = client.applications.generate_content(
          application_id="your-application-id",
          inputs=[
              {
                  "id": "Product name",
                  "value": [product_name]
              }
          ]
      )
      return response.suggestion
  ```

  ```javascript JavaScript theme={null}
  async function generateProductDescription(productName) {
      const response = await client.applications.generateContent(
          "your-application-id",
          {
              inputs: [
                  {
                      id: "Product name",
                      value: [productName]
                  }
              ]
          }
      );
      return response.suggestion;
  }
  ```
</CodeGroup>

### Define your agent as a tool

Next, define your agent as a tool so the LLM knows when to use it. See the [tool structure](#tool-structure) section for more details on the tool object.

<CodeGroup>
  ```python Python theme={null}
  tools = [
      {
          "type": "function",
          "function": {
              "name": "generate_product_description",
              "description": "A function that will invoke an agent for text-generation, specialized in generating product descriptions. Any user request asking for product descriptions should use this tool.",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "product_name": {
                          "type": "string",
                          "description": "The name of the product"
                      }
                  },
                  "required": ["product_name"]
              }
          }
      }
  ]
  ```

  ```javascript JavaScript theme={null}
  const tools = [
      {
          type: "function",
          function: {
              name: "generate_product_description",
              description: "A function that will invoke an agent for text-generation, specialized in generating product descriptions. Any user request asking for product descriptions should use this tool.",
              parameters: {
                  type: "object",
                  properties: {
                      product_name: {
                          type: "string",
                          description: "The name of the product"
                      }
                  },
                  required: ["product_name"]
              }
          }
      }
  ];
  ```
</CodeGroup>

### Use your agent as a tool in a chat completion

Add the tools array to the chat endpoint call along with your array of messages. Setting `tool_choice` to `auto` allows the model to choose when to use the no-code agent tool, based on the user's question and the description of the tool.

When the model decides to use the tool you've defined, it will indicate that in the `tool_calls` object of the response.

This example uses a non-streaming response. For a streaming implementation, and to learn more about processing custom tool call responses, see the [tool calling guide](/home/tool-calling#processing-tool-calls).

<CodeGroup>
  ```python Python theme={null}
  import json
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  messages = [{"role": "user", "content": "Generate a description for the Terra running shoe"}]

  # Send the request
  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto",
      stream=False
  )

  # Process the response
  response_message = response.choices[0].message
  # Check if the response contains tool calls,
  #and if so, call the custom function
  tool_calls = response_message.tool_calls
  if tool_calls:
      tool_call = tool_calls[0]
      tool_call_id = tool_call.id
      function_name = tool_call.function.name
      function_args = json.loads(tool_call.function.arguments)

      if function_name == "generate_product_description":
          function_response = generate_product_description(function_args["product_name"])
          
          messages.append({
              "role": "tool",
              "tool_call_id": tool_call_id,
              "name": function_name,
              "content": function_response
          })

  final_response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      stream=False
  )

  print(final_response.choices[0].message.content)
  # Here's a product description for the Terra running shoe: ...
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const messages = [{ role: "user", content: "Generate a description for the Terra running shoe" }];

  const response = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      tools: tools,
      tool_choice: "auto",
      stream: false
  });

  const responseMessage = response.choices[0].message;
  const toolCalls = responseMessage.tool_calls;
  // Check if the response contains tool calls,
  // and if so, call the custom function
  if (toolCalls && toolCalls.length > 0) {
      const toolCall = toolCalls[0];
      const toolCallId = toolCall.id;
      const functionName = toolCall.function.name;
      const functionArgs = JSON.parse(toolCall.function.arguments);

      if (functionName === "generate_product_description") {
          const functionResponse = await generateProductDescription(
              functionArgs.product_name
          );
          
          messages.push({
              role: "tool",
              tool_call_id: toolCallId,
              name: functionName,
              content: functionResponse
          });
      }

      const finalResponse = await client.chat.chat({
          model: "palmyra-x5",
          messages: messages,
          stream: false
      });

      console.log(finalResponse.choices[0].message.content);
      // Here's a product description for the Terra running shoe: ...
  }
  ```
</CodeGroup>

Here is the full code example:

<CodeGroup>
  ```python Python [expandable] theme={null}
  import json
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  def generate_product_description(product_name):
      response = client.applications.generate_content(
          application_id="your-application-id",
          inputs=[
              {
                  "id": "Product name",
                  "value": [product_name]
              }
          ]
      )
      return response.suggestion

  tools = [
      {
          "type": "function",
          "function": {
              "name": "generate_product_description",
              "description": "A function that will invoke an agent for text-generation, specialized in generating product descriptions. Any user request asking for product descriptions should use this tool.",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "product_name": {
                          "type": "string",
                          "description": "The name of the product"
                      }
                  },
                  "required": ["product_name"]
              }
          }
      }
  ]

  messages = [{"role": "user", "content": "Generate a description for the Terra running shoe"}]

  # Send the request
  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto",
      stream=False
  )

  # Process the response
  response_message = response.choices[0].message
  # Check if the response contains tool calls,
  #and if so, call the custom function
  tool_calls = response_message.tool_calls
  if tool_calls:
      tool_call = tool_calls[0]
      tool_call_id = tool_call.id
      function_name = tool_call.function.name
      function_args = json.loads(tool_call.function.arguments)

      if function_name == "generate_product_description":
          function_response = generate_product_description(function_args["product_name"])
          
          messages.append({
              "role": "tool",
              "tool_call_id": tool_call_id,
              "name": function_name,
              "content": function_response
          })

  final_response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      stream=False
  )

  print(final_response.choices[0].message.content)
  # Here's a product description for the Terra running shoe: ...
  ```

  ```javascript JavaScript [expandable] theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  async function generateProductDescription(productName) {
      const response = await client.applications.generateContent(
          "your-application-id",
          {
              inputs: [
                  {
                      id: "Product name",
                      value: [productName]
                  }
              ]
          }
      );
      return response.suggestion;
  }

  const tools = [
      {
          type: "function",
          function: {
              name: "generate_product_description",
              description: "A function that will invoke an agent for text generation, specialized in generating product descriptions. Any user request asking for product descriptions should use this tool.",
              parameters: {
                  type: "object",
                  properties: {
                      product_name: {
                          type: "string",
                          description: "The name of the product"
                      }
                  },
                  required: ["product_name"]
              }
          }
      }
  ];

  const messages = [{ role: "user", content: "Generate a description for the Terra running shoe" }];

  const response = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      tools: tools,
      tool_choice: "auto",
      stream: false
  });

  const responseMessage = response.choices[0].message;
  const toolCalls = responseMessage.tool_calls;
  // Check if the response contains tool calls,
  // and if so, call the custom function
  if (toolCalls && toolCalls.length > 0) {
      const toolCall = toolCalls[0];
      const toolCallId = toolCall.id;
      const functionName = toolCall.function.name;
      const functionArgs = JSON.parse(toolCall.function.arguments);

      if (functionName === "generate_product_description") {
          const functionResponse = await generateProductDescription(
              functionArgs.product_name
          );
          
          messages.push({
              role: "tool",
              tool_call_id: toolCallId,
              name: functionName,
              content: functionResponse
          });
      }

      const finalResponse = await client.chat.chat({
          model: "palmyra-x5",
          messages: messages,
          stream: false
      });

      console.log(finalResponse.choices[0].message.content);
      // Here's a product description for the Terra running shoe: ...
  }
  ```
</CodeGroup>

## Next steps

Now that you've defined a custom tool, explore other ways to extend your applications:

* [Ask questions to a Knowledge Graph in a chat](/home/kg-chat)
* [Delegate questions to another model](/home/model-delegation)
* [Analyze images in a chat](/home/chat-with-images)
* [Search the web](/home/web-search-tool)
* [Translate text in a chat](/home/translation-tool)


# Run agents asynchronously
Source: https://dev.writer.com/home/async-applications

Process no-code agent requests asynchronously or in batches with the async applications API. Generate content for multiple items efficiently.

<Info>No-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.</Info>

With asynchronous agents, your team can build and deploy [no-code agents in AI Studio](/no-code/introduction) and use the [async applications API](/api-reference/application-api/generate-application-job) to generate content asynchronously or in batches.

For example, your product team can build a no-code agent that creates product description pages, and then you can use the async applications API to generate pages in batches for multiple products.

This guide helps you understand how to run async jobs. The API is similar to the [applications API](/home/applications), but it allows you to process batches of requests asynchronously.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Endpoint overview

**URL:** `POST https://api.writer.com/v1/applications/{application_id}/jobs`

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.writer.com/v1/applications/<application-id>/jobs' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WRITER_API_KEY" \
  --data-raw '{
    "inputs": [
      {
        "id": "Input name",
        "value": [
          "Input value"
        ]
      }
    ]
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  job_response = client.applications.jobs.create(
      application_id="<application-id>",
      inputs=[
          {
              "id": "Input name",
              "value": [
                  "Input value"
              ]
          }
      ]
  )
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.applications.jobs.create(
    "<application-id>",
    {
      inputs: [
        {
          id: "Input name",
          value: [
            "Input value"
          ]
        }
      ]
    }
  )
  ```
</CodeGroup>

### Path parameters

| Parameter        | Type     | Description                 |
| ---------------- | -------- | --------------------------- |
| `application_id` | `string` | The ID of the no-code agent |

### Request body

The async applications API has the same request body structure as the [no-code agents API](/home/applications#request-body). It should contain an array of input objects matching the no-code agent's input schema.

### Response format

A successful job creation request returns a JSON object with the following structure:

| Parameter    | Type     | Description                                                            |
| ------------ | -------- | ---------------------------------------------------------------------- |
| `job_id`     | `string` | The ID of the job                                                      |
| `status`     | `string` | The status of the job. Can be `in_progress`, `completed`, or `failed`. |
| `created_at` | `string` | The date and time the job was created                                  |

```json theme={null}
{
  "job_id": "123-456-789",
  "status": "in_progress",
  "created_at": "2024-03-15T10:00:00Z"
}
```

## Usage example

The following example demonstrates using the async applications API to generate content asynchronously with a hypothetical product description generation agent.

### Create and deploy a no-code agent

First, create and deploy a no-code agent in [AI Studio](https://app.writer.com/aistudio). If you don't already have a no-code agent, follow the [text generation guide](/no-code/text-generation) to get started.

### Create an async job

Send a `POST` request with the inputs for the agent to create an async job.

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.writer.com/v1/applications/<application-id>/jobs' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WRITER_API_KEY" \
  --data-raw '{
    "inputs": [
      {
        "id": "Product descriptions",
        "value": [
          "Terra running shoe",
          "Aqua swim goggles",
          "Flex yoga mat"
        ]
      }
    ]
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  job = client.applications.jobs.create(
      application_id="<application-id>",
      inputs=[
          {
              "id": "Product descriptions",
              "value": [
                  "Terra running shoe",
                  "Aqua swim goggles",
                  "Flex yoga mat"
              ]
          }
      ],
  )
  print(f"Created job: {job.id}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const job = await client.applications.jobs.create(
    "<application-id>",
    {
      inputs: [
        {
          id: "Product descriptions",
          value: [
            "Terra running shoe",
            "Aqua swim goggles",
            "Flex yoga mat"
          ]
        }
      ]
    }
  );
  console.log(`Created job: ${job.id}`);
  ```
</CodeGroup>

### Check job status

Use the job ID from the creation response to check the status of your job and see the results.

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.writer.com/v1/applications/jobs/<job-id>' \
  -H "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  job_status = client.applications.jobs.retrieve("<job-id>")
  print(f"Job status: {job_status.status}")
  if job_status.status == "completed":
      print(f"Job results: {job_status.data.suggestion}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const jobStatus = await client.applications.jobs.retrieve("<job-id>");
  console.log(`Job status: ${jobStatus.status}`);
  if (jobStatus.status === "completed") {
    console.log(`Job results: ${jobStatus.data.suggestion}`);
  }
  ```
</CodeGroup>

The response includes the current status. If the job has completed, the results are in the `data.suggestion` field, which has the same structure as the [no-code agents API](/home/applications#response-format).

```json theme={null}
{
    "id": "123-456-789",
    "status": "completed",
    "application_id": "2932402-23429023894-234234234",
    "created_at": "2025-02-10T18:18:09.501223Z",
    "completed_at": "2025-02-10T18:18:14.470324Z",
    "data": {
        "title": "Social post",
        "suggestion": "# Social post\nImage: A photo of a person running in a pair of Terra running shoes.\n\nCaption:\n\nIntroducing the all-new Terra running shoe, designed to take you further than ever before. With its innovative cushioning system and durable outsole, the Terra is perfect for runners of all levels.\n\nWhether you're hitting the trails or pounding the pavement, the Terra will keep you comfortable and supported mile after mile. So what are you waiting for? Lace up a pair of Terras and start your journey today!\n\n#TerraRunningShoe #RunFurther #NeverStopExploring #GetOutside"
    },
    "error": null
}
```

### List all jobs

You can retrieve all jobs for an application to monitor batch processing:

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.writer.com/v1/applications/<application-id>/jobs' \
  -H "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  jobs = client.applications.jobs.list(
      application_id="<application-id>"
  )
  for job in jobs.result:
      print(f"Job {job.id}: {job.status}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const jobs = await client.applications.jobs.list(
    "<application-id>"
  );
  jobs.result.forEach(job => {
    console.log(`Job ${job.id}: ${job.status}`);
  });
  ```
</CodeGroup>

### Retry failed jobs

If a job fails, you can retry it using the retry endpoint:

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.writer.com/v1/applications/jobs/<failed-job-id>/retry' \
  -X POST \
  -H "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  retry_response = client.applications.jobs.retry("<failed-job-id>")
  print(f"Retried job: {retry_response.id}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const retryResponse = await client.applications.jobs.retry("<failed-job-id>");
  console.log(`Retried job: ${retryResponse.id}`);
  ```
</CodeGroup>

## Next steps

By following this guide, you can use the async Applications API to handle large-scale tasks.

Next, learn how to enhance your no-code agents with [Knowledge Graph](/home/knowledge-graph), our tool for RAG.


# Engineering Blog
Source: https://dev.writer.com/home/blog





# Changelog
Source: https://dev.writer.com/home/changelog

Stay up to date with the latest changes to the Writer API and SDKs.

<Tip>To see the latest features and major improvements across the Writer platform, check out [What's new at Writer](https://support.writer.com/article/40-whats-new-at-writer).</Tip>

<Update label="2026-06-02" description="Python and Node SDK v3.0.0">
  ## Released version 3.0.0 of the Python and Node SDKs

  Version 3.0.0 of the [Python](https://github.com/writer/writer-python) and [Node](https://github.com/writer/writer-node) SDKs includes the following changes:

  * Removes AI detection, Medical Comprehend, and Context-Aware Text Splitting from the Python and Node SDKs
  * Removes deprecated MCP tool schemes from the Node SDK MCP server

  See below for more details about each of these changes.

  ### Removed API methods

  Version 3.0.0 removes the following methods from the Python and Node SDKs. You cannot call these methods in 3.0.0:

  * AI detection
  * [Medical Comprehend](/api-reference/migration-guides/comprehend-medical)
  * Context-Aware Text Splitting

  If your application uses any of these methods, update your code before upgrading to 3.0.0.

  ### Node SDK

  The Node SDK MCP server now only supports **code mode**.

  Removed tool schemes:

  * **All tools** (`--tools=all`): exposed one MCP tool per API endpoint
  * **Dynamic tools** (`--tools=dynamic`): exposed tools to discover and invoke API endpoints dynamically

  Removed flags also include `--resources`, `--tags`, `--client`, and related filtering and compatibility options. Before upgrading to 3.0.0, remove those flags from your startup command and invoke the server with `npx -y writer-sdk-mcp@latest` or `node /path/to/mcp/server`.

  Supported flags are `--port`, `--transport`, `--socket`, and `--tools=docs`.
</Update>

<Update label="2026-02-06" description="Python SDK v2.4.0">
  ## Released version 2.4.0 of the Python SDK

  Version 2.4.0 of the [Python SDK](https://github.com/writer/writer-python) includes updated default retry behavior and Knowledge Graph file upload support.

  ### Default retry behavior

  Retry defaults are now:

  * **Maximum retries**: 7 attempts
  * **Initial delay**: 1 second
  * **Maximum delay**: 60 seconds

  Override these defaults by passing `max_retries` when instantiating the client. See the [Python SDK documentation](https://github.com/writer/writer-python) for details.

  ### Knowledge Graph file upload support

  You can now pass the `graphId` parameter when [uploading files](/home/files#upload-a-file) to associate files with a Knowledge Graph during upload. When you provide a `graphId`, the uploaded file is automatically attached to the specified Knowledge Graph.

  <CodeGroup>
    ```python Python theme={null}
    from pathlib import Path
    from writerai import Writer

    # Initialize the Writer client. If you don't pass the `api_key` parameter,
    # the client looks for the `WRITER_API_KEY` environment variable.
    client = Writer()

    file_path = Path("<FILE_PATH>")  # Replace with your file path, for example: /path/to/document.pdf
    file_name = file_path.name  # Get the filename from the path
    file_ = client.files.upload(
      content=file_path.read_bytes(),
      content_disposition=f"attachment; filename={file_name}",  # Replace with your file name, for example: document.pdf
      content_type="application/pdf",
      graph_id="<GRAPH_ID>"  # Replace with your Knowledge Graph ID, for example: 6029b226-1ee0-4239-a1b0-cdeebfa3ad5a
    )

    print(file_.id)
    ```

    ```javascript JavaScript theme={null}
    import fs from 'fs';
    import { Writer } from "writer-sdk";

    // Initialize the Writer client. If you don't pass the `api_key` parameter,
    // the client looks for the `WRITER_API_KEY` environment variable.
    const client = new Writer();

    const file = await client.files.upload({
      content: fs.createReadStream("<FILE_PATH>"),  // Replace with your file path, for example: /path/to/document.pdf
      "Content-Disposition": "attachment; filename=<FILE_NAME>",  // Replace with your file name, for example: document.pdf
      "Content-Type": "application/pdf", // Replace with your file type, for example: application/pdf
      graphId: "<GRAPH_ID>"  // Replace with your Knowledge Graph ID, for example: 6029b226-1ee0-4239-a1b0-cdeebfa3ad5a
    });

    console.log(file.id);
    ```
  </CodeGroup>

  For more information, see the [file management guide](/home/files#upload-a-file).
</Update>

<Update label="2025-12-02" description="External models and guardrails">
  ## External models and guardrails now available

  You can now add external models from providers like AWS Bedrock to use alongside Palmyra models in AI Studio. You can also configure guardrails from third-party providers such as AWS Bedrock to monitor and filter agent inputs and outputs, helping enforce content safety and compliance.

  ### External models

  Add models from AWS Bedrock directly in the AI Studio UI. Once configured, external models appear in the [list models endpoint](/api-reference/completion-api/list-models) and can be used in chat completions by specifying the model ID.

  To use an external model, pass its model ID to the `model` parameter in your API requests:

  ```bash theme={null}
  curl -X POST https://api.writer.com/v1/chat \
    -H "Authorization: Bearer $WRITER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "your-external-model-id",
      "messages": [{"role": "user", "content": "Hello!"}]
    }'
  ```

  For setup instructions, see the [external models guide](/home/external-models).

  ### Guardrails

  Configure AWS Bedrock Guardrails to enforce content safety, PII protection, and compliance policies across your AI agents. Guardrails can evaluate requests before, during, or after model calls.

  For setup instructions, see the [guardrails guide](/home/guardrails).
</Update>

<Update label="2025-10-03" description="Python and Node SDK v2.3.2">
  ## Released version 2.3.2 of the Python and Node SDKs

  Version 2.3.2 of the [Python](https://github.com/writer/writer-python) and [Node](https://github.com/writer/writer-node) SDKs adds support for the following new features:

  ### Knowledge Graph query configuration

  Added support for the `query_config` parameter in both chat completions with Knowledge Graph tools and direct Knowledge Graph queries. This parameter allows you to fine-tune search behavior and control response generation.

  Key configuration options:

  * **`inline_citations`**: Enable inline citations within response text
  * **`search_weight`**: Control the balance between keyword and semantic search
  * **`grounding_level`**: Control how closely responses match source material
  * **`max_snippets`**: Set the number of text snippets to retrieve
  * **`keyword_threshold`**: Set keyword matching strictness
  * **`semantic_threshold`**: Set semantic similarity requirements

  ### Inline citations

  SDK support for inline citations in Knowledge Graph responses, allowing you to programmatically extract and correlate citations with their source material. Citations appear in the format `[filename](cite_id)` within response text and you can match them with source data from the `references` object.

  ### Upload types for no-code applications

  Added support for the `upload_types` parameter in no-code applications, allowing you to see which file types you can upload through file input fields.

  For more details on these features:

  * Knowledge Graph query configuration: [Knowledge Graph query configuration guide](/home/knowledge-graph-query-config)
  * Inline citations: [Work with inline citations](/home/inline-citations)
  * Upload types: [No-code applications API reference](/api-reference/application-api/application-details)
</Update>

<Update label="2025-09-11" description="Knowledge Graph query configuration and inline citations">
  ## Knowledge Graph query configuration and inline citations

  Added comprehensive query configuration options for Knowledge Graph operations, allowing you to fine-tune search behavior and enable inline citations in responses from the [Knowledge Graph chat completions tool](/home/kg-chat) and [direct Knowledge Graph queries](/home/kg-query).

  ### New query configuration parameters

  The `query_config` parameter is now available for both chat completions with Knowledge Graph tools and direct Knowledge Graph queries. This parameter includes:

  * **`inline_citations`**: Enable inline citations within response text, showing which sources support each part of the response
  * **`search_weight`**: Control the balance between keyword and semantic search (0-100, default: 50)
  * **`max_subquestions`**: Set the maximum number of sub-questions for complex queries (1-10, default: 6)
  * **`grounding_level`**: Control how closely responses match source material (0.0-1.0, default: 0.0)
  * **`max_snippets`**: Set the number of text snippets to retrieve (5-25 recommended, default: 30)
  * **`max_tokens`**: Control the maximum response length (100-8000, default: 4000)
  * **`keyword_threshold`**: Set keyword matching strictness (0.0-1.0, default: 0.7)
  * **`semantic_threshold`**: Set semantic similarity requirements (0.0-1.0, default: 0.7)

  For detailed information about all configuration options and their effects, see the [Knowledge Graph query configuration guide](/home/knowledge-graph-query-config). For more information about inline citations, see the [Work with inline citations in Knowledge Graph responses](/home/inline-citations) guide.
</Update>

<Update label="2025-08-20" description="Python and Node SDK v2.3.1">
  ## Released version 2.3.1 of the Python and Node SDKs

  This patch release fixes a bug introduced in version 2.3.0 where the `Graph` class wasn't accessible. The `Graph` class is now available in the Python and Node SDKs as it was previously.

  For more information on working with Knowledge Graphs with the Python and Node SDKs, see the [Knowledge Graphs usage guide](/home/knowledge-graph).
</Update>

<Update label="2025-08-19" description="langchain-writer v0.3.3">
  ## langchain-writer v0.3.3

  The [langchain-writer](https://github.com/writer/langchain-writer) package now includes the [web search tool](/home/web-search-tool) for chat completions. This tool allows you to search the web for current information during a conversation with a Palmyra model.

  For more details, see the [langchain-writer changelog](https://github.com/writer/langchain-writer/releases/tag/v0.3.3).
</Update>

<Update label="2025-08-14" description="Python and Node SDK v2.3.0">
  ## Released version 2.3.0 of the Python and Node SDKs

  Version 2.3.0 of the [Python](https://github.com/writer/writer-python) and [Node](https://github.com/writer/writer-node) SDKs adds support for the following new features:

  * [Web search API](#web-search-api)
  * [Web search tool for chat completions](#web-search-tool-for-chat-completions)
  * [Web connector URLs for Knowledge Graphs](#web-connector-urls-for-knowledge-graphs)
  * [Image support in chat completions](#image-support-in-chat-completions)

  See below for more details about each of these features.

  ### Web search and web connector URLs

  Three new features are now available to enhance your AI applications with web content and search capabilities:

  * [Web search tool API](#web-search-tool-api)
  * [Web search tool for chat completions](#web-search-tool-for-chat-completions)
  * [Web connector URLs for Knowledge Graphs](#web-connector-urls-for-knowledge-graphs)

  #### Web search API

  The new [web search tool API](/home/web-search) allows you to search the web for current information and get real-time results. This tool is useful for finding factual information, news, and data that may not be available in your model's training data.

  Key features:

  * Search for current information and news
  * Filter results by domain, time range, and geographic location
  * Control search depth and result comprehensiveness
  * Support for both general and news-specific searches

  #### Web search tool for chat completions

  The [web search prebuilt tool](/home/web-search-tool) for chat completions enables your AI assistant to search the web during conversations with Palmyra models. This allows your AI to provide up-to-date information and answer questions about current events.

  #### Web connector URLs for Knowledge Graphs

  [Web connector URLs](/home/web-connector-url) allow you to automatically extract and index content from websites into your Knowledge Graphs. This enables you to:

  * Process single pages or entire sub-pages
  * Monitor the status of URL processing
  * Exclude specific URLs from processing
  * Query web content through your Knowledge Graph

  ### Image support in chat completions

  Added support for [mixed content in chat completions](/home/chat-with-images) with Palmyra X5, allowing you to include images directly in your chat messages. This feature enables rich visual conversations without needing to use the separate Vision tool.

  ### References

  For more information, see the following API reference docs and guides:

  * Web search tool: [API reference](/api-reference/tool-api/web-search) and [guide](/home/web-search)
  * Web search tool for chat completions: [API reference](/api-reference/completion-api/chat-completion#body-tools) and [guide](/home/web-search-tool)
  * Web connector URLs for Knowledge Graphs: [API reference](/api-reference/kg-api/update-graph#body-urls) and [guide](/home/web-connector-url)
  * Chat with images: [API reference](/api-reference/completion-api/chat-completion#body-messages-content) and [guide](/home/chat-with-images)
</Update>

<Update label="2025-08-12" description="Vision chat support for Palmyra X5">
  ### Vision chat support for Palmyra X5

  Added support for [mixed content in chat completions](/home/chat-with-images) with Palmyra X5, allowing you to include images directly in your chat messages. This feature enables rich visual conversations without needing to use the separate Vision tool.

  Key capabilities:

  * Send messages containing both text and images
  * Support for multiple images in a single message
  * Use of data URLs for local images
  * Natural conversation flow with visual context

  Below is an example of a chat completion request that includes a message with both text and an image.

  <CodeGroup>
    ```bash cURL theme={null}
    curl --location 'https://api.writer.com/v1/chat' \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --data '{
      "model": "palmyra-x5",
      "messages": [
        {
          "role": "user",
          "content": [
            {
              "type": "text",
              "text": "What do you see in this image?"
            },
            {
              "type": "image_url",
              "image_url": {
                "url": "https://example.com/image.jpg"
              }
            }
          ]
        }
      ]
    }'
    ```

    ```python Python theme={null}
    from writerai import Writer

    # Initialize the client. If you don't pass the `apiKey` parameter,
    # the client looks for the `WRITER_API_KEY` environment variable.
    client = Writer()

    response = client.chat.chat(
      model="palmyra-x5",
      messages=[
        {
          "role": "user",
          "content": [
            {
              "type": "text",
              "text": "What do you see in this image?"
            },
            {
              "type": "image_url",
              "image_url": {
                "url": "https://example.com/image.jpg"
              }
            }
          ]
        }
      ]
    )

    print(response.choices[0].message.content)
    ```

    ```javascript JavaScript theme={null}
    import { Writer } from "writer-sdk";

    // Initialize the client. If you don't pass the `apiKey` parameter,
    // the client looks for the `WRITER_API_KEY` environment variable.
    const client = new Writer();

    const response = await client.chat.chat({
      model: "palmyra-x5",
      messages: [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: "What do you see in this image?"
            },
            {
              type: "image_url",
              image_url: {
                url: "https://example.com/image.jpg"
              }
            }
          ]
        }
      ]
    });

    console.log(response.choices[0].message.content);
    ```
  </CodeGroup>
</Update>

<Update label="2025-01-27" description="Web search and web connector URLs released">
  ## Web search and web connector URLs released

  Three new features are now available to enhance your AI applications with web content and search capabilities:

  * [Web search tool API](#web-search-tool-api)
  * [Web search tool for chat completions](#web-search-tool-for-chat-completions)
  * [Web connector URLs for Knowledge Graphs](#web-connector-urls-for-knowledge-graphs)

  ### Web search tool API

  The new [web search tool API](/home/web-search) allows you to search the web for current information and get real-time results. This tool is useful for finding factual information, news, and data that may not be available in your model's training data.

  Key features:

  * Search for current information and news
  * Filter results by domain, time range, and geographic location
  * Control search depth and result comprehensiveness
  * Support for both general and news-specific searches

  ### Web search tool for chat completions

  The [web search prebuilt tool](/home/web-search-tool) for chat completions enables your AI assistant to search the web during conversations with Palmyra models. This allows your AI to provide up-to-date information and answer questions about current events.

  ### Web connector URLs for Knowledge Graphs

  [Web connector URLs](/home/web-connector-url) allow you to automatically extract and index content from websites into your Knowledge Graphs. This enables you to:

  * Process single pages or entire sub-pages
  * Monitor the status of URL processing
  * Exclude specific URLs from processing
  * Query web content through your Knowledge Graph

  For more information, see the following guides:

  * [Web search tool API documentation](/api-reference/tool-api/web-search)
  * [Web search tool for chat completions](/home/web-search-tool)
  * [Web connector URLs for Knowledge Graphs](/home/web-connector-url)
</Update>

<Update label="2025-07-16" description="Writer Strands Agents integration released">
  ## Writer Strands Agents integration released

  You can now use Writer's models with [AWS' Strands Agents](https://github.com/strands-agents/sdk-python), an open-source SDK for building and deploying AI agents. See the new [Strands integration guide](/home/integrations/strands) for setup instructions, configuration, and examples.
</Update>

<Update label="2025-07-16" description="2.2.1 of the Python and Node SDKs">
  ## Released version 2.2.1 of the Python and Node SDKs

  Version 2.2.1 of the [Python](https://github.com/writer/writer-python) and [Node](https://github.com/writer/writer-node) SDKs adds support for filtering files by file type.

  To filter files by type, use the `file_types` parameter to pass a comma-separated string of file extensions (without the dot) to the `files.list` method. The following code shows how to filter for only PDF and DOCX files:

  <CodeGroup>
    ```python Python theme={null}
    files = client.files.list(file_types="pdf,docx")
    ```

    ```javascript JavaScript theme={null}
    const files = await client.files.list({ file_types: 'pdf,docx' });
    ```
  </CodeGroup>

  For more information on the `files.list` method and its parameters, refer to the [List Files API documentation](/api-reference/file-api/get-all-files).
</Update>

<Update label="2025-07-10" description="instructor-writer supports JSON output">
  ## Writer Instructor integration now supports JSON output

  Added support for the `WRITER_JSON` mode in the [Instructor integration](/home/integrations/instructor#json-mode). This mode allows you to receive structured output directly as JSON, following your specified schema, making it easier to work with validated, structured data from LLM responses.
</Update>

<Update label="2025-05-12" description="langchain-writer v0.3.1">
  ## langchain-writer v0.3.1

  The [langchain-writer](https://github.com/writer/langchain-writer) package now includes the [translation tool](/home/translation-tool) for chat completions. This tool allows you to translate text between languages during chats with Palmyra LLMs.

  For more details, see the [langchain-writer changelog](https://github.com/writer/langchain-writer/releases/tag/v0.3.1).
</Update>

<Update label="2025-05-02" description="Filter files by type">
  ## Filter files by type

  The [`/v1/files` endpoint](/api-reference/file-api/get-all-files#parameter-file-types) now supports filtering files by extension type.

  To filter files by type, pass a comma-separated string of file extensions to the `file_types` query parameter. For example, `file_types=txt,pdf,docx`.

  <CodeGroup>
    ```bash cURL theme={null}
    curl --location --request GET "https://api.writer.com/v1/files?file_types=txt,pdf,docx" \
      --header "Authorization: Bearer $WRITER_API_KEY" \
    ```
  </CodeGroup>

  The `file_types` parameter will be available in the next version of the Python and Node SDKs.
</Update>

<Update label="2025-04-30" description="Python and Node SDK v2.2.0">
  ## Released version 2.2.0 of the Python and Node SDKs

  Version 2.2.0 of the [Python](https://github.com/writer/writer-python) and [Node](https://github.com/writer/writer-node) SDKs adds support the following features:

  * [Structured output](/home/structured-output) for the chat completions endpoint
  * [AI detection endpoint](/home/ai-detect)
  * [Text translation endpoint](/home/translation)
  * [The text translation tool](/home/translation-tool) for chat completions

  The [langchain-writer](https://github.com/writer/langchain-writer) package has been updated to support structured output. See the [langchain-writer changelog](https://github.com/writer/langchain-writer/releases/tag/v0.3.0) for more details.

  See the API reference documentation for information about the new endpoints and tools:

  * [Structured output for chat completions](/api-reference/completion-api/chat-completion#body-response-format)
  * [AI detection](/api-reference/tool-api/ai-detect)
  * [Text translation](/api-reference/translation-api/translate)
  * [The text translation tool](/api-reference/completion-api/chat-completion#body-tools)
</Update>

<Update label="2025-04-29" description="Palmyra X5 and X4 on Amazon Bedrock">
  ## Palmyra X5 and X4 now available on Amazon Bedrock

  Palmyra X5 and X4 are now available on [Amazon Bedrock](https://aws.amazon.com/bedrock/).

  Amazon Bedrock is a fully managed service from AWS that enables developers to build and scale generative AI applications using foundation models from leading AI companies. This integration allows you to use Palmyra X5 and X4 in Amazon's serverless environment.

  See the following resources for more information:

  * [Guide for setting up and using Palmyra X5 and X4 on Amazon Bedrock](/home/integrations/bedrock)
  * [AWS blog post about using Palmyra X5 and X4 on Amazon Bedrock](https://aws.amazon.com/blogs/aws/writer-palmyra-x5-and-x4-foundation-models-are-now-available-in-amazon-bedrock/)
</Update>

<Update label="2025-04-28" description="Palmyra X5">
  ## Palmyra X5 model now available

  The [Palmyra X5 model](/home/models#palmyra-x5) is now available via the API and SDKs.

  Palmyra X5 is Writer's newest and most advanced model for building and scaling AI agents, featuring a 1 million token context window, adaptive reasoning, and industry-leading speed and cost efficiency.

  <Info>As part of this release, Palmyra X 004 is now called Palmyra X4.</Info>

  Palmyra X5's 1M token context window further streamlines enterprise workflows and unlocks complex, multi-step use cases that weren't possible before, such as:

  **Multi-step agentic workflows**

  * **Support documentation**: Classify requests, assess urgency, assign a human review, stage updates in a CMS, and publish after approval.
  * **Fund reporting**: Streamline the analysis and preparation of detailed reports on the performance and status of investment funds, using reporting and research data pulled in from third-party systems
  * **Content lifecycle management**: Flag content that could be outdated, generate suggested revisions, and share them for human review.

  **Large data requirements**:

  * **Customer feedback analysis**: Analyze large volumes of customer feedback to identify common themes, summarize sentiments, and generate actionable insights.
  * **Research and development**: Process and summarize multiple technical reports, research papers, and experimental data, accelerating innovation and product development.
  * **Financial reporting**: Process and summarize annual reports, SEC filings, and market analysis reports together at once to extract financial data, identify trends, and generate executive summaries.
  * **Legal document analysis**: Analyze lengthy legal documents, including contracts, patents, and compliance reports, to identify key clauses, flag potential risks, and ensure regulatory compliance.
  * **Medical records analysis**: Analyze and summarize large files of medical records containing structured and unstructured data, including patient records, clinical trial reports, audit reports, and more.

  See the [Palmyra X5 model overview](/home/models#palmyra-x5) for more details and [examples of using Palmyra X5 with the text generation and chat completion endpoints](/home/models#using-models-with-the-writer-api).
</Update>

<Update label="2025-04-15" description="langchain-writer supports Python 3.9">
  ## Writer LangChain integration now supports Python 3.9

  The [Writer LangChain integration](/home/integrations/langchain) now supports versions of Python 3.9 and higher, as of [version 0.2.0](https://github.com/writer/langchain-writer/releases/tag/v0.2.0).
</Update>

<Update label="2025-04-10" description="Terminology updates">
  ## Terminology updates

  As part of Writer's [recent product announcement](https://writer.com/blog/writer-ai-hq), no-code applications are now called [no-code agents](/no-code/introduction). The [Applications API](api-reference/application-api/applications), which you can use to programmatically interact with no-code agents, still uses the term `application` to minimize breaking changes.

  The guides for [invoking no-code apps](/home/applications), [async applications](/home/async-applications), and [using no-code apps as tools](/home/applications-tool-calling) have been updated to reflect the new terminology. However, the [Applications API](/api-reference/application-api/applications) reference documentation still reflects previous terminology.
</Update>

<Update label="2025-04-04" description="Python and Node SDK v2.1.0">
  ## Released version 2.1.0 of the Python and Node SDKs

  This release adds support for the [vision endpoint](/api-reference/vision-api/analyze-images) and the [vision tool](/home/vision-tool) for chat completions.

  It also adds streaming helper methods to process streaming responses from the chat completions endpoint.

  * [Streaming helpers in Python](https://github.com/writer/writer-python?tab=readme-ov-file#streaming-helpers)
  * [Streaming helpers in Node](https://github.com/writer/writer-node?tab=readme-ov-file#streaming-helpers)
</Update>

<Update label="2025-03-26" description="New cookbooks">
  ## New cookbooks for Palmyra models

  We've added three new [cookbooks](/home/cookbooks#palmyra-models) to help you explore domain-specific Palmyra models for creative, financial, and medical use cases.

  * [Palmyra Creative](https://github.com/writer/cookbooks/blob/main/models/palmyra_creative.ipynb)
  * [Palmyra Fin](https://github.com/writer/cookbooks/blob/main/models/palmyra_fin.ipynb)
  * [Palmyra Med](https://github.com/writer/cookbooks/blob/main/models/palmyra_med.ipynb)
</Update>

<Update label="2025-03-20" description="New vision endpoint">
  ## Palmyra Vision now available via the API

  The Palmyra Vision model is now available via the API with the [`/vision` endpoint](/api-reference/vision-api/analyze-images).

  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST \
      'https://api.writer.com/v1/vision' \
      -H 'Content-Type: application/json' \
      -H "Authorization: Bearer $WRITER_API_KEY" \
      --data-raw '{
        "model": "palmyra-vision",
        "prompt": "What's the difference between the image {{image_1}} and the image {{image_2}}?",
        "variables": [
          {"name": "image_1", "file_id": "f1234"},
          {"name": "image_2", "file_id": "f5678"}
        ]
      }'
    ```
  </CodeGroup>

  The `/vision` endpoint analyzes images based on a prompt. You can use the endpoint for a variety of use cases, including:

  * Extracting handwritten text
  * Interpreting charts and graphs
  * Generating product descriptions

  Find additional use cases in the [Palmyra Vision overview](https://writer.com/llms/palmyra-vision/).

  See the [vision API guide](/home/analyze-images) for a detailed walkthrough of uploading images and passing them to the `/vision` endpoint, or check out the full [vision API reference](/api-reference/vision-api/analyze-images).
</Update>

<Update label="2025-03-10" description="langchain-writer v0.1.0">
  ## Released langchain-writer v0.1.0

  * Updated tool binding to align with standard LangChain tool calling conventions.
  * Added a new `NoCodeAppTool` tool for using no-code applications as tools.
  * Added a new `LLMTool` tool for delegating to a specific Palmyra model.

  See the [guide to using the LangChain integration](/home/integrations/langchain) and the [package documentation](https://github.com/writer/langchain-writer/tree/main/docs) for more details.
</Update>

<Update label="2025-03-05" description="New cookbooks">
  ## New cookbooks for tool calling and no-code applications

  We've added three new [cookbooks](/home/cookbooks) to help you get started with no-code applications and tool calling.

  * [Generating content from no-code applications with the SDK](https://github.com/writer/cookbooks/blob/main/applications/application_basic_usage.ipynb)
  * [Delegating a medical question to the `palmyra-med` model](https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_llm.ipynb)
  * [Using the Knowledge Graph chat tool](https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_kg.ipynb)
</Update>

<Update label="2025-03-03" description="palmyra-fin">
  ## Palmyra Fin model updates

  Palmyra Fin, our financial model, now has a 128k context window, up from 32k.

  To use the Palmyra Fin model in the API and SDKs, use the `palmyra-fin` model ID. The `palmyra-fin-32k` model ID is now deprecated.
</Update>

<Update label="2025-02-28" description="langchain-writer v0.0.3">
  ## Released Writer LangChain integration

  We're excited to announce the first release of `langchain-writer`, the official Writer LangChain integration. This release includes:

  * The `ChatWriter` model for text generation
  * Tool calling capabilities, including the `GraphTool` for retrieving information from a Knowledge Graph
  * Additional tools like the `PDFParser` for parsing PDFs and the `WriterTextSplitter` for intelligent text splitting

  Check out the [tutorial](/home/integrations/langchain) to get started and the full [integration documentation](https://github.com/writer/langchain-writer/blob/main/docs/) for more details.
</Update>

<Update label="2025-02-27" description="Python and Node SDK v2.0.0">
  ## Released version 2.0.0 of the Python and Node SDKs

  The 2.0.0 release includes new functionality for the Writer SDKs for Python and Node:

  * **Model delegation tool**: A prebuilt, remotely executed tool that lets you delegate domain-specific requests to the appropriate Palmyra model. For example, if your application has finance questions, you can use the tool to allow Palmyra X4 to delegate these questions to Palmyra Financial. Read more in our [model delegation guide](/home/model-delegation).
  * **Retrieve application details**: Retrieve details for all of your no-code applications. Programmatically retrieve application inputs rather than relying on the AI Studio UI. This pairs well with our guide on [using no-code applications as tools](/home/applications).
  * **Async jobs**: Trigger no-code application content generation asynchronously and retrieve or retry the job at a later time. This is great for long-running no-code applications, such as research assistant applications. To learn more, check out the [guide on async jobs](/home/async-applications).
  * **Associate Knowledge Graphs with chat applications**: Retrieve or update which Knowledge Graphs are associated with no-code chat applications.

  <Warning>
    The 2.0.0 release includes some breaking changes to the underlying models used in chat completion. This won't affect you if you are only using chat completion via the `Writer.chat.chat` method, but it will if you are importing specific types directly. See below for more details about changes in each SDK.
  </Warning>

  See the SDK repositories for the full changelogs:

  * [Python](https://github.com/writer/writer-python/releases)
  * [Node](https://github.com/writer/writer-node/releases)

  ### Python SDK

  See below for the mapping of old types to new types:

  | Old                                                 | New                                                  |
  | --------------------------------------------------- | ---------------------------------------------------- |
  | `chat_chat_params.Tool`                             | `types.shared_params.tool_param.ToolParam`           |
  | `types.Chat`                                        | `types.chat_completion.ChatCompletion`               |
  | `types.chat.ChoiceMessageGraphDataSource`           | `types.shared.source.Source`                         |
  | `types.chat.ChoiceMessageGraphDataSubquerySource`   | `types.shared.source.Source`                         |
  | `types.chat_completion_chunk.ChoiceDeltaGraphData`  | `types.shared.graph_data.GraphData`                  |
  | `types.chat_completion_chunk.Usage`                 | `types.chat_completion_usage`                        |
  | `types.chat_completion_chunk.ChoiceMessageToolCall` | `types.shared.tool_call_streaming.ToolCallStreaming` |
  | `types.question.Source`                             | `types.shared.source.Source`                         |
  | `types.question.SubquerySource`                     | `types.shared.source.Source`                         |

  Additionally:

  * The `types.chat_completion_chunk.Message` type has been moved to its own model: `types.chat_completion_message`.

  * `types.chat.ChoiceLogprobsContent`, `ChoiceLogprobsRefusal`, `ChoiceLogprobsContentTopLogprob`, and `ChoiceLogprobsRefusalTopLogprob` have all been deduplicated and replaced by a shared model `types.shared.logprobs_token.LogprobsToken`.

  ### Node SDK

  See below for the mapping of old types to new types:

  | Old                                          | New                                |
  | -------------------------------------------- | ---------------------------------- |
  | `ChatResource`                               | `Chat`                             |
  | `Chat`                                       | `ChatCompletion`                   |
  | `Chat.Choice`                                | `ChatCompletionChoice`             |
  | `Chat.Choice.Message`                        | `ChatCompletionMessage`            |
  | `Chat.Choice.Logprobs`                       | `Shared.Logprobs`                  |
  | `Chat.Choice.Logprobs.Content`               | `Shared.LogprobsToken`             |
  | `Chat.Choice.Logprobs.Refusal`               | `Shared.LogprobsToken`             |
  | `Chat.Usage`                                 | `ChatCompletionUsage`              |
  | `ChatCompletionChunk.Choice.Message`         | `ChatCompletionMessage`            |
  | `ChatCompletionChunk.Choice.Delta`           | `ChatCompletionChunk.Choice.Delta` |
  | `ChatCompletionChunk.Choice.Delta.GraphData` | `Shared.GraphData`                 |
  | `ChatCompletionChunk.Choice.Delta.ToolCall`  | `Shared.ToolCallStreaming`         |
  | `ChatCompletionChunk.Choice.Logprobs`        | `Shared.Logprobs`                  |
  | `ChatCompletionChunk.Usage`                  | `ChatCompletionUsage`              |
  | `ChatChatParams.Message.GraphData`           | `Shared.GraphData`                 |
  | `ChatChatParams.Message.ToolCall`            | `Shared.ToolCall`                  |
  | `ChatChatParams.StringToolChoice`            | `Shared.ToolChoiceString`          |
  | `ChatChatParams.JsonObjectToolChoice`        | `Shared.ToolChoiceJsonObject`      |
  | `ChatChatParams.FunctionTool`                | `Shared.ToolParam.FunctionTool`    |
  | `ChatChatParams.GraphTool`                   | `Shared.ToolParam.GraphTool`       |
  | `StreamingData`                              | `CompletionChunk`                  |
  | `Completion.Choice.LogProbs`                 | `Shared.Logprobs`                  |

  The Node SDK includes developer experience improvements from [Stainless](https://www.stainlessapi.com/), including using the native `fetch` API, resulting in zero SDK dependencies.
</Update>

<Update label="2024-12-20" description="Python SDK v1.6.1">
  ## Released version 1.6.1 of the Python SDK

  * Fixed a typing bug where the `content` field in the `File` did not accept the correct `FileTypes` values.
</Update>

<Update label="2024-12-16" description="Python SDK v1.6.0; Node SDK v1.5.0">
  ## Released Python SDK version 1.6.0 and Node SDK version 1.5.0

  * Added streaming support to the [applications content generation endpoint](/api-reference/application-api/applications) and updated the [No-code applications guide](/home/applications) to reflect this.
</Update>

<Update label="2024-11-29" description="Python SDK v.1.5.0">
  ## Released Python SDK version 1.5.0

  * Added new functionality via manual API updates
  * Fixed compatibility with the latest `httpx` 0.28.0 release
  * Improved handling of deprecated arguments (`transport`, `proxies`) to maintain backward compatibility
  * Fixed compatibility issues with Pydantic `model_dump` method when warnings are passed
  * Removed the now unused `cached-property` dependency, as we now rely on the standard library implementation for Python 3.8+
  * Enhanced logging documentation in README, adding information about the 'info' log level
  * Updated the recommended log level from 'debug' to 'info' for standard usage
  * Fixed code formatting for better readability and consistency
  * Excluded test files from `mypy` checks to avoid false positives
  * Updated internal compatibility layer for better type checking
  * Improved error handling for deprecated client configuration options
</Update>

<Update label="2024-11-14" description="Python and Node SDK v1.4.0">
  ## Released Python and Node SDK versions 1.4.0

  * Updated the default timeout on the Writer client to three minutes.
</Update>


# Generate chat completions
Source: https://dev.writer.com/home/chat-completion

Create multi-turn conversations with AI using the chat completion API. Preserve conversation context and use tool calling for advanced features.

The [chat completion endpoint](/api-reference/completion-api/chat-completion) allows you to create a conversation between a user and an AI-assisted chat model.

This guide introduces the chat completion endpoint and shows how to create a multi-turn conversation with an LLM, where the conversation history is preserved so the model understands the context of the conversation.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Chat completion versus text generation

The chat completion endpoint is similar to the [text generation endpoint](/home/text-generation), but it is designed to handle conversations between a user and an LLM.

The chat completion endpoint can generate single messages, or create more complex conversations between a user and an LLM. The text generation endpoint is designed to generate a single text response based on a given prompt.

Additionally, the chat completion endpoint offers [tool calling](/home/tool-calling), which you can use to access other LLMs, Knowledge Graphs, and custom functions.

## Endpoint overview

**URL:** `POST https://api.writer.com/v1/chat`

<Warning>
  Using the `/chat` endpoint results in charges for **model usage**. See the [pricing page](/home/pricing) for more information.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
      "model": "palmyra-x5",
      "messages": [
          {
              "role": "user",
              "content": "You are an expert at writing concise product descriptions for an E-Commerce Retailer"
          },
          {
              "role": "assistant",
              "content": "Okay, great I can help write these descriptions. Do you have a specific product in mind?"
          },
          {
              "role": "user",
              "content": "Please write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions"
          }
      ]
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  chat_response = client.chat.chat(
    messages=[
      {
          "role": "user",
          "content": "You are an expert at writing concise product descriptions for an E-Commerce Retailer"
      },
      {
          "role": "assistant",
          "content": "Okay, great I can help write these descriptions. Do you have a specific product in mind?"
      },
      {
          "role": "user",
          "content": "Please write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions"
      }
    ],
    model="palmyra-x5"
  )

  print(chat_response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();  

  const chatResponse = await client.chat.chat({
    messages: [
      {
          role: "user",
          content: "You are an expert at writing concise product descriptions for an E-Commerce Retailer"
      },
      {
          role: "assistant",
          content: "Okay, great I can help write these descriptions. Do you have a specific product in mind?"
      },
      {
          role: "user",
          content: "Please write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions"
      }
    ],
    model: 'palmyra-x5'
  });

  console.log(chatResponse.choices[0].message.content);
  ```
</CodeGroup>

### Request body

Below are the required and commonly used optional parameters for the text generation endpoint.

| Parameter            | Type    | Description                                                                                                                                                                                                                                                                                                                       |
| -------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`              | string  | **Required**. The [ID of the model](/home/models) to use for the chat completion. This can be a Palmyra model like `palmyra-x5` or `palmyra-x4`, or an [external model](/home/external-models) configured for your organization.                                                                                                  |
| `messages`           | array   | **Required**. The conversation history.                                                                                                                                                                                                                                                                                           |
| `messages[].role`    | string  | **Required**. The role of the message sender. Can be `user`, `assistant`, `system`, or `tool`. `system` messages are system prompts, used to provide instructions to the model. `tool` messages are the result of a [tool call](/home/tool-calling#append-the-result-back-to-the-model), and contain the output of the tool call. |
| `messages[].content` | string  | **Required**. The content of the message.                                                                                                                                                                                                                                                                                         |
| `temperature`        | float   | Temperature influences the randomness in generated text. Defaults to `1`. Increase the value for more creative responses, and decrease the value for more predictable responses.                                                                                                                                                  |
| `stream`             | Boolean | A Boolean value that indicates whether to stream the response. Defaults to `false`.                                                                                                                                                                                                                                               |

See the [chat completion endpoint reference](/api-reference/completion-api/chat-completion) for more information on the request body and the additional parameters you can use to control the conversation.

### Response parameters

#### Non-streaming response

If you set the `stream` parameter to `false`, the response is delivered as a single JSON object. It contains several parameters describing the response, including the `choices` array, which contains the generated text.

| Parameter                    | Type   | Description                                                                        |
| ---------------------------- | ------ | ---------------------------------------------------------------------------------- |
| `model`                      | string | The ID of the model used to generate the response.                                 |
| `choices`                    | array  | An array containing one object with the generated text and additional information. |
| `choices[0].message.content` | string | The generated text.                                                                |

See the full list of response parameters in the [chat completion endpoint reference](/api-reference/completion-api/chat-completion).

```json non-streaming response [expandable] theme={null}
{
  "id": "f7aed821-58cf-4210-9d73-538b2cb8ae44",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "content": "Elevate your wardrobe with this versatile, cozy sweater that seamlessly transitions from casual days to formal evenings.",
        "role": "assistant",
        "tool_calls": null,
        "graph_data": {
          "sources": null,
          "status": null,
          "subqueries": null
        },
        "llm_data": null,
        "image_data": null,
        "refusal": null
      },
      "logprobs": null
    }
  ],
  "created": 1741891192,
  "model": "palmyra-x5",
  "usage": {
    "prompt_tokens": 96,
    "total_tokens": 118,
    "completion_tokens": 22,
    "prompt_token_details": null,
    "completion_tokens_details": null
  },
  "system_fingerprint": "v1",
  "service_tier": null
}
```

#### Streaming response

If you set the `stream` parameter to `true`, the response is delivered as [server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events). The event contains several parameters. The content of the chunk is in the `choices[0].delta.content` parameter.

| Parameter                  | Type   | Description               |
| -------------------------- | ------ | ------------------------- |
| `choices[0].delta.content` | string | The content of the chunk. |

```python streaming response [expandable] theme={null}
data: {'id': '3bb941e2-4dab-4ceb-b4e3-45a429f40c72',
 'object': 'chat.completion.chunk',
 'choices': [{'index': 0,
   'finish_reason': None,
   'message': {'content': 'This',
    'role': 'assistant',
    'tool_calls': None,
    'graph_data': {'sources': None, 'status': None, 'subqueries': None},
    'llm_data': None,
    'image_data': None,
    'refusal': None},
   'delta': {'content': 'This',
    'role': 'assistant',
    'tool_calls': None,
    'graph_data': {'sources': None, 'status': None, 'subqueries': None},
    'llm_data': None,
    'image_data': None,
    'refusal': None},
   'logprobs': None}],
 'created': 1741891257,
 'model': 'palmyra-x5',
 'usage': None,
 'system_fingerprint': 'v1',
 'service_tier': None}
```

## Sample application

The following sample application uses the Python and JavaScript SDKs to create a command-line chatbot.

The application asks the user for input, passes the conversation history to the LLM, and streams the response from the LLM. It loops until the user enters the message `exit`.

### Set a system prompt

To guide the behavior of the assistant, you can set a system prompt by adding a message with the role `system` to the `messages` array.

For example, you can set a system prompt to have the assistant respond in a certain tone or style, or to provide additional context for the conversation. Here's a system prompt that directs the assistant to be casual and use emojis in its responses:

<CodeGroup>
  ```python Python theme={null}
  system_prompt = "You are a helpful assistant that responds in a casual, friendly tone and uses emojis in your responses."

  messages = [
    {"role": "system", "content": system_prompt},
  ]
  ```

  ```javascript JavaScript theme={null}
  const system_prompt = "You are a helpful assistant that responds in a casual, friendly tone and uses emojis in your responses.";

  let messages = [
    {role: "system", content: system_prompt},
  ];
  ```
</CodeGroup>

Learn about [prompting best practices](/home/prompting) to help you create effective system prompts.

### Stream chat responses

The sample application streams the responses from the LLM. Streaming improves the user experience, showing the input as it is generated and reducing the latency of the final response.

Below are the code snippets from the sample application to stream the chat responses. They use the [`stream` helper method of the `chat` endpoint](/home/streaming#streaming-helpers-for-chat-completions) to print the chat responses to the console in real time.

The `stream` method also collects the final response and returns it so it can be added to the conversation history.

<CodeGroup>
  ```python Python theme={null}
  # Stream the chat response to the user using the `stream` helper method
  with client.chat.stream(messages=messages, model="palmyra-x5") as stream:
      for event in stream:
          # Check if the event is a content delta, which contains this chunk of the chat response
          if event.type == "content.delta":
              print(event.delta, end="", flush=True)
  # Collect the full response from the stream
  completion = stream.get_final_completion().choices[0].message.content
  ```

  ```javascript JavaScript theme={null}
  // Stream the chat response to the user using the `stream` helper method
  const stream = client.chat.stream({
      model: 'palmyra-x5',
      messages: messages,
    })
    // Print this chunk of the chat response to the console
    .on('content', (diff) => process.stdout.write(diff));

  // Collect the full response from the stream
  let full_message = await stream.finalChatCompletion();
  full_message = full_message.choices[0].message.content;
  ```
</CodeGroup>

### Full application

The following is the complete sample application that uses the functions defined above to stream the chat responses.

The application sets a system prompt, asks the user for an initial message, and then enters a loop to handle the conversation between the user and the LLM. It adds the user's message to the conversation history and streams the response from the LLM to the user.

The loop continues until the user enters the message `exit`.

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  end = False
  system_prompt = "You are a helpful assistant that responds in a casual, friendly tone and uses emojis in your responses."

  # Ask the user for an initial message
  input_message = "\nEnter a message for the assistant. Type 'exit' to end the conversation. > "
  initial_message = input(input_message)

  # Add the user's message to the conversation history
  messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": initial_message}]

  # Main loop to handle the conversation.
  while not end:
      # Stream the chat response to the user using the `stream` helper method
      with client.chat.stream(messages=messages, model="palmyra-x5") as stream:
          for event in stream:
              # Check if the event is a content delta, which contains this chunk of the chat response
              if event.type == "content.delta":
                  print(event.delta, end="", flush=True)
      # Collect the full response from the stream and add it to the conversation history
      full_message = stream.get_final_completion().choices[0].message.content
      messages.append({"role": "assistant", "content": full_message})
      new_message = input(input_message)
      messages.append({"role": "user", "content": new_message})
      if new_message == "exit":
          end = True
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();  

  // helpers for reading user input from the command line
  // ----------------------------------------------------------------------------
  const readline = await import('readline');

  // Create an interface for input and output
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  // Prompt the user for input.
  const prompt = (question) => {
    return new Promise((resolve) => {
      rl.question(question, (answer) => {
        resolve(answer);
      });
    });
  };
  // ----------------------------------------------------------------------------

  let end = false;
  let system_prompt = "You are a helpful assistant that responds in a casual, friendly tone and uses emojis in your responses.";
  let initial_message = await prompt("Enter a message for the assistant. Type 'exit' to end the conversation. > ");
  let messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": initial_message}];

  // Main loop to handle the conversation.
  while (!end) {
    // Stream the chat response to the user using the `stream` helper method
    const stream = client.chat.stream({
      model: 'palmyra-x5',
      messages: messages,
    })
    // Print this chunk of the chat response to the console
    .on('content', (diff) => process.stdout.write(diff));

    // Collect the full response from the stream and add it to the conversation history
    let full_message = await stream.finalChatCompletion();
    full_message = full_message.choices[0].message.content;
    messages.push({role: "assistant", content: full_message});
    console.log("\n");
    let new_message = await prompt("Enter a message for the assistant. Type 'exit' to end the conversation. > ");
    messages.push({role: "user", content: new_message});
    if (new_message === "exit") {
        end = true;
    }
  }
  ```
</CodeGroup>

## Best practices

Follow these best practices to ensure that your chatbot behaves as expected:

* **Use system messages**: Including a system message can guide the behavior of the assistant, setting expectations for its tone and responsiveness.
* **Maintain context**: Ensure that all relevant parts of the conversation are included in the `messages` array to maintain context, as the model doesn't retain memory of past interactions.
* **Handle errors gracefully**: Implement [error handling](/api-reference/error-handling) for various HTTP status codes and API-specific errors such as rate limits or malformed requests.
* **Manage conversational flow**: Regularly review the conversation's context and adjust it to keep interactions relevant and concise, especially under the model's token limit.

## Next steps

Now that you've created a chatbot, learn how to [add tool calling](/home/tool-calling) to your application to enhance the functionality with other LLMs, Knowledge Graphs, and custom functions.

* [Create custom functions as tools](/home/tool-calling)
* [Access information in Knowledge Graphs](/home/kg-chat)
* [Delegate tasks to another model](/home/model-delegation)


# Add images to chat completions
Source: https://dev.writer.com/home/chat-with-images



You can include images directly in your chat conversations with Palmyra X5 using mixed content support. This allows you to send both text and images in the same message, enabling rich visual conversations with Palmyra X5.

<Warning>
  Image analysis in chat completions is only supported with the **Palmyra X5** model. Other models don't support the `image_url` content type.
</Warning>

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Overview

Mixed content support allows you to send messages that contain both text and images in a chat with Palmyra X5. You can include images directly in your chat messages, making conversations more natural and contextual.

## How it works

When sending a message to the chat completion endpoint, you can use the `content` field in two ways:

1. **Text-only message**:

```json theme={null}
    "messages": [
        {
            "role": "user",
            "content": "Hello, how are you?"
        }
    ]
```

2. **Mixed content message**:

```json theme={null}
    "messages": [
        {
            "role": "user",
            "content": [{"type": "text", "text": "What do you see in this image?"}, {"type": "image_url", "image_url": {"url": "<IMAGE_URL>"}}]
        }
    ]
```

## Endpoint

**URL:** `POST https://api.writer.com/v1/chat`

### Content fragment types

#### Text fragment

```json theme={null}
{
  "type": "text",
  "text": "Your text content here"
}
```

#### Image fragment

```json theme={null}
{
  "type": "image_url",
  "image_url": {
    "url": "<IMAGE_URL>"
  }
}
```

## Examples

### Single image analysis

This example shows how to send a message with a single image. The `content` field is an array of content fragments, where each fragment can be either text or image.

The text fragment includes the message for the model to analyze. The image fragment includes the image URL.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
    "model": "palmyra-x5",
    "messages": [
      {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "What do you see in this image?"
          },
          {
            "type": "image_url",
            "image_url": {
              "url": "<IMAGE_URL>"
            }
          }
        ]
      }
    ]
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.chat.chat(
    model="palmyra-x5",
    messages=[
      {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "What do you see in this image?"
          },
          {
            "type": "image_url",
            "image_url": {
              "url": "<IMAGE_URL>"
            }
          }
        ]
      }
    ]
  )

  print(response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.chat.chat({
    model: "palmyra-x5",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "What do you see in this image?"
          },
          {
            type: "image_url",
            image_url: {
              url: "<IMAGE_URL>"
            }
          }
        ]
      }
    ]
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

### Using local images with data URLs

You can also use [data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) to send local images without uploading them to a remote server. This is useful for testing or when working with local files:

<CodeGroup>
  ```bash cURL theme={null}
  # Convert local image to base64 data URL
  base64_image=$(base64 -i "<IMAGE_PATH>")
  data_url="data:image/jpeg;base64,$base64_image"

  curl --location 'https://api.writer.com/v1/chat' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data "{
    \"model\": \"palmyra-x5\",
    \"messages\": [
      {
        \"role\": \"user\",
        \"content\": [
          {
            \"type\": \"text\",
            \"text\": \"Analyze this local image:\"
          },
          {
            \"type\": \"image_url\",
            \"image_url\": {
              \"url\": \"$data_url\"
            }
          }
        ]
      }
    ]
  }"
  ```

  ```python Python theme={null}
  import base64
  from pathlib import Path
  from writerai import Writer

  # Initialize the client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  # Read local image and convert to base64 data URL
  def image_to_data_url(image_path):
      with open(image_path, "rb") as image_file:
          encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
          file_extension = Path(image_path).suffix.lstrip('.')
          mime_type = f"image/{file_extension}" if file_extension in ['jpg', 'jpeg', 'png', 'gif'] else "image/jpeg"
          return f"data:{mime_type};base64,{encoded_string}"

  # Convert local image to data URL
  data_url = image_to_data_url("<IMAGE_PATH>")

  response = client.chat.chat(
    model="palmyra-x5",
    messages=[
      {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "Analyze this local image:"
          },
          {
            "type": "image_url",
            "image_url": {
              "url": data_url
            }
          }
        ]
      }
    ]
  )

  print(response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";
  import fs from 'fs';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  // Convert local image to base64 data URL
  function imageToDataUrl(imagePath) {
    const imageBuffer = fs.readFileSync(imagePath);
    const base64String = imageBuffer.toString('base64');
    const fileExtension = imagePath.split('.').pop();
    const mimeType = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension) 
      ? `image/${fileExtension}` 
      : 'image/jpeg';
    return `data:${mimeType};base64,${base64String}`;
  }

  // Convert local image to data URL
  const dataUrl = imageToDataUrl("<IMAGE_PATH>");

  const response = await client.chat.chat({
    model: "palmyra-x5",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "Analyze this local image:"
          },
          {
            type: "image_url",
            image_url: {
              url: dataUrl
            }
          }
        ]
      }
    ]
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

### Multiple images with text

You can include multiple images in a single message:

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
    "model": "palmyra-x5",
    "messages": [
      {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "Compare these two images and tell me the differences:"
          },
          {
            "type": "image_url",
            "image_url": {
              "url": "<IMAGE_URL_1>"
            }
          },
          {
            "type": "image_url",
            "image_url": {
              "url": "<IMAGE_URL_2>"
            }
          }
        ]
      }
    ]
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.chat.chat(
    model="palmyra-x5",
    messages=[
      {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "Compare these two images and tell me the differences:"
          },
          {
            "type": "image_url",
            "image_url": {
              "url": "<IMAGE_URL_1>"
            }
          },
          {
            "type": "image_url",
            "image_url": {
              "url": "<IMAGE_URL_2>"
            }
          }
        ]
      }
    ]
  )

  print(response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.chat.chat({
    model: "palmyra-x5",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "Compare these two images and tell me the differences:"
          },
          {
            type: "image_url",
            image_url: {
              url: "<IMAGE_URL_1>"
            }
          },
          {
            type: "image_url",
            image_url: {
              url: "<IMAGE_URL_2>"
            }
          }
        ]
      }
    ]
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

## Next steps

* Learn about [chat completions](/home/chat-completion) for text-based conversations
* Check out [Palmyra X5](/home/models#palmyra-x5) model capabilities


# Cookbooks
Source: https://dev.writer.com/home/cookbooks



[Writer Cookbooks](https://github.com/writer/cookbooks) are step-by-step guides to help you implement tasks and solve common problems using Writer. Each recipe includes code snippets, explanations, and best practices to enhance your understanding of the Writer platform.

## Featured

<CardGroup>
  <Card title="Multimodal chat completion" icon="square-1" href="https://github.com/writer/cookbooks/blob/main/completion/multimodal_chat_x5.ipynb">
    Send both text and images in the same message to enable rich, visual conversations with Palmyra X5.
  </Card>

  <Card title="Third-party models" icon="square-2" href="https://github.com/writer/cookbooks/blob/main/models/Amazon_Bedrock.ipynb">
    Call third-party Amazon Bedrock models via the Writer SDK, including chat and tool calling.
  </Card>

  <Card title="Knowledge Graph" icon="square-3" href="https://github.com/writer/cookbooks/blob/main/knowledge_graph/knowledge_graph.ipynb">
    Use our RAG implementation to create, manage, and use structured data to enhance AI capabilities.
  </Card>
</CardGroup>

## All cookbooks

### Applications

<CardGroup>
  <Card title="Working with no-code agents" href="https://github.com/writer/cookbooks/blob/main/applications/application_basic_usage.ipynb">
    Retrieve no-code agent metadata and generate output from them using the Writer SDK.
  </Card>

  <Card title="Knowledge Graph management" href="https://github.com/writer/cookbooks/blob/main/applications/application_graph_management.ipynb">
    Attach a Knowledge Graph to a no-code agent programmatically.
  </Card>

  <Card title="Run no-code agents asynchronously" href="https://github.com/writer/cookbooks/blob/main/applications/application_jobs_utilization.ipynb">
    Run no-code agents asynchronously and manage asynchronous jobs.
  </Card>
</CardGroup>

### Completions

<CardGroup>
  <Card title="Text completion" href="https://github.com/writer/cookbooks/blob/main/completion/text_completion.ipynb">
    Use the Writer Python SDK for text completion tasks.
  </Card>

  <Card title="Chat completion" href="https://github.com/writer/cookbooks/blob/main/completion/chat_completion.ipynb">
    Build single-turn and multi-turn chat completions to support conversational AI applications.
  </Card>

  <Card title="Multimodal chat completion" href="https://github.com/writer/cookbooks/blob/main/completion/multimodal_chat_x5.ipynb">
    Send both text and images in the same message to enable rich, visual conversations with Palmyra X5.
  </Card>

  <Card title="Structured completions" href="https://github.com/writer/cookbooks/blob/main/completion/structured_output_cookbook.ipynb">
    Get predictable, machine-readable responses from Palmyra LLMs using JSON Schema or Pydantic objects.
  </Card>
</CardGroup>

### Knowledge Graph

<CardGroup>
  <Card title="Knowledge Graph" href="https://github.com/writer/cookbooks/blob/main/knowledge_graph/knowledge_graph.ipynb">
    Use our RAG implementation to create, manage, and use structured data to enhance AI capabilities.
  </Card>

  <Card title="Knowledge Graph tool calling" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_kg.ipynb">
    Access a Knowledge Graph in a chat.
  </Card>
</CardGroup>

### Integrations

<CardGroup>
  <Card title="Instructor basics" href="https://github.com/writer/cookbooks/blob/main/integrations/instructor/Instructor.ipynb">
    Extract structured data from text, CSV, and PDF files using Instructor and Writer.
  </Card>

  <Card title="Advanced Instructor" href="https://github.com/writer/cookbooks/blob/main/integrations/instructor/Advanced.ipynb">
    Work with nested Pydantic models, retry and repair patterns, and streaming using Instructor and Writer.
  </Card>

  <Card title="LangChain basics" href="https://github.com/writer/cookbooks/blob/main/integrations/langchain/LangChain.ipynb">
    Use Writer with LangChain to build chat workflows and LangGraph agents with tool calling.
  </Card>

  <Card title="Advanced LangChain" href="https://github.com/writer/cookbooks/blob/main/integrations/langchain/Advanced.ipynb">
    Build Writer-powered LangChain agents with custom middleware, context engineering, and MCP.
  </Card>

  <Card title="OpenLLMetry" href="https://github.com/writer/cookbooks/blob/main/integrations/OpenLLMetry.ipynb">
    Monitor, trace, and debug Writer API calls using OpenLLMetry and Traceloop.
  </Card>

  <Card title="Strands Agents" href="https://github.com/writer/cookbooks/blob/main/integrations/Strands.ipynb">
    Build agents with the Strands Agents SDK and Writer models, either through direct integration or Amazon Bedrock.
  </Card>
</CardGroup>

### Models

<CardGroup>
  <Card title="List all models" href="https://github.com/writer/cookbooks/blob/main/models/model_retrieval.ipynb">
    List all of the Palmyra models available in the Writer API.
  </Card>

  <Card title="Third-party models" href="https://github.com/writer/cookbooks/blob/main/models/Amazon_Bedrock.ipynb">
    Call third-party Amazon Bedrock models via the Writer SDK, including chat and tool calling.
  </Card>
</CardGroup>

### Tool calling

<CardGroup>
  <Card title="Tool calling fundamentals" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_api.ipynb">
    Learn how to use tool calling to enhance the functionality of your LLM.
  </Card>

  <Card title="LLM tool calling" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_llm.ipynb">
    Use model delegation to refer a task to another model during a chat.
  </Card>

  <Card title="Knowledge Graph tool calling" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_kg.ipynb">
    Access a Knowledge Graph in a chat.
  </Card>

  <Card title="Streaming tool calls" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_streaming.ipynb">
    Use tool calling with streaming responses.
  </Card>

  <Card title="Math tool calling example" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_calling_math.ipynb">
    Extend the capabilities of your LLM with predefined mathematical functions.
  </Card>

  <Card title="Translation tool calling" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_translation_chat.ipynb">
    Translate text between different languages in chat completions.
  </Card>

  <Card title="Web search tool calling" href="https://github.com/writer/cookbooks/blob/main/tool_calling/tool_web_search.ipynb">
    Answer real-time questions by retrieving up-to-date information from the internet during a chat.
  </Card>
</CardGroup>

### Tools

<CardGroup>
  <Card title="PDF parser" href="https://github.com/writer/cookbooks/blob/main/tools/pdf_parser.ipynb">
    Parse a PDF file and extract the text as text or markdown.
  </Card>
</CardGroup>


# Custom connectors
Source: https://dev.writer.com/home/custom-connectors

Create custom connectors from OpenAPI specifications or MCP servers to extend WRITER Agent with your own APIs and tools

This guide shows you how to create custom connectors for WRITER Agent. Custom connectors enable integration with internal tools and third-party services not available in the [prebuilt connector library](/home/mcp-gateway#available-connectors).

You can create custom connectors in two ways:

* **OpenAPI specification**: Upload an OpenAPI definition to expose your API endpoints as tools for WRITER Agent.
* **MCP server**: Connect to an external MCP server to make its tools available in WRITER Agent.

## Create a custom connector from an OpenAPI specification

### Prerequisites

Before creating an OpenAPI-based custom connector, make sure you have:

* An [OpenAPI definition](https://learn.openapis.org/specification/structure.html) that describes your API
* **Authentication details** for your API, based on the authentication method you choose:
  * **API key**: Key name and optional key prefix
  * **OAuth**: Authorization server URL, access token URL, refresh token URL, and token endpoint authentication method

### Set up the connector

Create a custom connector in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**.

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select **Create custom connector**
3. Choose **OpenAPI specification** as the connector type
4. Upload your OpenAPI specification file (JSON or YAML format)
5. Enter a **Connector name** and **About this connector** description
6. Configure authentication:
   * **API key**: Enter the key name and optional key prefix
   * **OAuth**: Enter your OAuth credentials (authorization server URL, access token URL, refresh token URL, and token endpoint authentication method)
7. Select **Next** to proceed to [tool configuration](#configure-tools)

### Configure tools

After uploading your [OpenAPI specification](#create-a-custom-connector-from-an-openapi-specification), the tool configuration screen lets you review and edit the operations defined in your specification.

For each tool, you can:

* **Edit the tool description**: Double-click or select the pencil icon to modify the description. Clear descriptions help WRITER Agent understand when to use each tool.
* **Set the operation type**: Specify whether the tool performs a read, write, update, or delete operation.
* **Update the tool name**: Customize the name that appears in WRITER Agent.

The **Usage** column shows an estimated token count for each tool, including the tool name, description, and parameters. Use this to identify high-cost tools and reduce token usage.

After configuring your tools, select **Save** to create the connector.

### Configure the connector in AI Studio

After [creating an OpenAPI-based custom connector](#set-up-the-connector), configure it for your organization:

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Find your custom connector and select **Configure**
3. Select who has access by default (all users or specific teams)
4. Select which tools to enable
5. Enter your API key or OAuth credentials, depending on the authentication method configured for the connector

After configuration, the connector appears in your connected integrations and is available to authorized users in WRITER Agent.

## Create a custom connector from an MCP server

Connect to an external MCP server to make its tools available in WRITER Agent. The MCP server connector supports three authentication methods:

* **No auth**: Connect without authentication.
* **API key**: Authenticate with an API key.
* **OAuth**: Authenticate with OAuth 2.0, at the user level or org level.

### Create the connector

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select **Create custom connector**
3. Choose **MCP server** as the connector type
4. Enter the MCP server URL
5. Enter a **Connector name** and **About this connector** description
6. Select **Next** to proceed to configuring the connector in AI Studio

### Configure the connector in AI Studio

After entering your MCP server details, the configure step lets you set up access control and authentication:

1. Update the **Profile name** or keep the default
2. Select who has access by default (all teams or specific teams)
3. Select the authentication method for the MCP server:
   * **No auth**: No credentials required. Select **Next** to complete the connection.
   * **API key**: Enter the API key details to authenticate with the server.
   * **OAuth**: Enter your OAuth credentials, including scopes, client ID, and client secret. Select whether authentication is at the user level (each user authenticates individually) or org level (shared connection for the organization).
4. Complete the connection flow

After configuration, the connector appears in your connected integrations and is available to authorized users in WRITER Agent.

<Note>
  To route a connector over a private network instead of the public internet,
  see [Private endpoints](/home/private-endpoints).
</Note>

## Edit a custom connector

Modify an existing custom connector to update its name, description, or tool configurations.

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Find your custom connector and select **Edit connector**
3. Update the connector name or description as needed
4. Select **Next** to modify tool configurations:
   * Edit tool descriptions
   * Update operation types
   * Modify tool names
5. Select **Save** to apply your changes

<Note>
  When editing a custom connector, you cannot change the connector type (OpenAPI
  specification or MCP server), the uploaded specification, or the
  authentication type. To use a different specification or connector type,
  create a new custom connector.
</Note>

## Delete a custom connector

Deleting a custom connector removes it from your organization's library and disconnects all users.

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Find your custom connector and select **Edit connector** (under the three vertical dots)
3. Select **Delete connector**
4. Confirm the deletion

<Warning>
  Deleting a custom connector removes it from your library and disconnects all
  users. This action cannot be undone.
</Warning>

## Next steps

* [Configure connectors](/home/mcp-gateway): Learn about connector authentication and access control
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools


# Choose your development path
Source: https://dev.writer.com/home/development_options

Choose your development path for AI agents. Compare no-code builder, Agent Builder, and Writer API/SDKs for customization and technical requirements.

<Tip>
  Need help building? Writer has an in-house solutions team that can build custom
  AI agents for you. **[Contact our sales team](https://go.writer.com/contact-sales)**
</Tip>

Writer offers multiple development paths to build AI agents and applications, each designed for different technical backgrounds and use cases. Whether you're a business user who wants to build agents without coding, a Python developer looking for a visual editor with backend flexibility, or an engineer who needs full API control, Writer provides the right tools for your project. Each option is powered by the same Writer generative AI platform but differs in code requirements, customization scope, and development approach.

## Development options comparison

Determine which path is right for you by thinking about what you want to achieve and your technical background:

| Platform        | Best for                                                                        | Coding required                                                        | Customization scope                                                                   |
| --------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| No-code builder | Simple AI agents for content generation, chat, and research                     | None. Build with prompts and visual configuration only                 | Moderate. Chain prompts, configure outputs, and adjust settings through UI            |
| Agent Builder   | Complex workflows with UI components, data processing, and multi-step logic     | Optional. Use visual blocks or add custom Python for advanced features | Extensive. Drag-and-drop UI builder, workflow automation, and custom code integration |
| Writer API/SDKs | Integrating AI capabilities into existing applications or building from scratch | Advanced. Full programming knowledge required                          | Unlimited. Complete control over implementation and user experience                   |

## Choose your development path

<Tabs>
  <Tab title="Agent Builder">
    <Check>
      Agent Builder is ideal for anyone who wants to build complex AI agents using visual tools with optional coding.
    </Check>

    **With Agent Builder, you can:**

    * Build AI agents using a drag-and-drop visual editor
    * Add Python code to your agent to build custom logic
    * Use external data, like APIs, to bring in information from other sources
    * Easily integrate Writer platform capabilities like Knowledge Graphs and tool calling using prebuilt blocks
    * Quickly analyze and visualize data using an LLM

    ### Getting started resources

    * [Agent Builder overview](/agent-builder/overview)
    * [Demo agent walkthrough](/agent-builder/demo-agent)
    * [Agent Builder quickstart](/agent-builder/quickstart)

    ### Guides

    * [Chatbot tutorial](/agent-builder/chatbot-tutorial)
    * [Process invoices and send to Slack](/agent-builder/invoice-processing)
    * [Upload, parse, and summarize PDFs](/agent-builder/summarize-pdfs)
    * [Tool calling](/agent-builder/tool-calling)

    ### Example use cases

    <AccordionGroup>
      <Accordion title="Customer support chatbot" icon="comments">
        Build a chatbot to answer frequently asked questions and help customers with their inquiries.
      </Accordion>

      <Accordion title="Patient portal" icon="hospital-user">
        Generate SOAP notes and extract ICD codes from patient-doctor chat.
      </Accordion>

      <Accordion title="Employee onboarding coordinator" icon="user-plus">
        Guide new hires through personalized onboarding checklists, automatically provision accounts and access, and track completion progress across departments.
      </Accordion>

      <Accordion title="Contract review assistant" icon="file-contract">
        Analyze legal documents for key terms, potential risks, and compliance issues, then generate summary reports for legal teams.
      </Accordion>

      <Accordion title="Customer support triage agent" icon="headset">
        Automatically classify incoming support tickets by urgency and topic, then route them to the appropriate team with suggested responses.
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="No-code">
    <Check>
      No-code tools are ideal for business users who want to quickly build an AI agent without writing any lines of code.
    </Check>

    **With the No-code builder, you can:**

    * Build AI agents using a visual editor
    * Build agents with content generation or editing capabilities
    * Create internal tools for content teams
    * Create agents with chat capabilities that can also connect to a Knowledge Graph of your data
    * Create agents that can research, search the web, and prepare reports
    * Use any available model, including Palmyra and external models

    ### Quickstart guides

    To start building, follow a quickstart for your specific use case:

    * [Text generation](/no-code/text-generation)
    * [Chat](/no-code/chat)
    * [Research](/no-code/research)

    ### Example use cases

    <AccordionGroup>
      <Accordion title="Knowledge assistants" icon="book">
        Using [Knowledge Graph](https://writer.com/product/graph-based-rag/), our graph-based RAG solution, you can build chat assistants to quickly ask questions using your data sources.
      </Accordion>

      <Accordion title="Campaign workflow automation" icon="arrow-progress">
        Automate an entire campaign workflow and generate all launch assets from a single messaging document or GTM presentation.
      </Accordion>
    </AccordionGroup>

    <div>
      <a href="https://app.writer.com/aistudio"><button>Start building</button></a>
      <a href="/no-code"><button>**Learn more**</button></a>
    </div>
  </Tab>

  <Tab title="Writer API & SDKs">
    <Check>
      Writer API and SDKs are ideal for engineers who want to embed generative AI into their own stack or access the Writer platform via an API, regardless of the programming language.
    </Check>

    **With the Writer APIs, you can:**

    * Use chat and completion APIs to build custom apps, or build custom integrations with your existing tech stack.
    * Create knowledge graphs using your own files and documents, allowing you to build intelligent Q\&A systems, content recommendation engines, and document retrieval systems that understand context.
    * Extend the LLM's functionality by enabling it to call your custom functions with tool calling. The model can use these functions to fetch real-time data or perform calculations when generating responses.
    * Use the RESTful API with any language you prefer, or use our Python SDK or Node SDK.
    * **Note:** You will need an API key to use Writer APIs/SDKs. [Follow the instructions here to get your API key](/api-reference/api-keys).

    ### Getting started resources

    * [API Quickstart](/home/quickstart)
    * [Getting started with Writer SDKs](/home/sdks)
      * [Python SDK](https://pypi.org/project/writer-sdk/) (`pip install writer-sdk`)
        * We also have several [Python cookbooks](https://github.com/writer/cookbooks) available to help you get started with common tasks.
      * [Node SDK](https://www.npmjs.com/package/writer-sdk) (`npm install writer-sdk`)

    ### Guides

    * [Text generation](/home/text-generation)
    * [Chat completion](/home/chat-completion)
    * [Knowledge Graph](/home/knowledge-graph)
    * [Applications API](/home/applications)
    * [Tool calling](/home/tool-calling)
    * [Knowledge Graph chat](/home/kg-chat)

    ### Example use cases

    <AccordionGroup>
      <Accordion title="Integrated chat assistant" icon="code">
        Embed a chat app into an existing tool or service.
      </Accordion>

      <Accordion title="Integrated text completion" icon="text">
        Add text completion capabilities to an existing tool or service your company already uses.
      </Accordion>
    </AccordionGroup>

    <div>
      <a href="https://app.writer.com/aistudio"><button>Start building</button></a>
      <a href="/home/quickstart"><button>**Learn more**</button></a>
    </div>
  </Tab>
</Tabs>

## Start from a template

When building a new agent, you can choose to start from scratch or accelerate your development with a prebuilt template. These templates provide ready-to-use prompts, configurations, and workflows tailored to specific business needs and use cases. There are templates for no-code agents and Agent Builder agents.

<img alt="" />

### Template categories

**Starter templates** are core building blocks for common agent types:

* **Text Generation**: Build [no-code agents](/no-code/text-generation) that collect user inputs and generate customized text
* **Chat**: Construct [conversational no-code agents](/no-code/chat) with specific personas connected to your data sources
* **Research**: Create [no-code agents](/no-code/research) for conducting tailored research and analysis
* **New Agent**: Start completely from scratch with [Agent Builder](/agent-builder/overview) when no template fits your needs

**Industry-specific templates** are specialized agents designed for particular sectors:

* **Financial Services**: From broker research Q\&A and 10-K summaries to fund compliance and market research
* **Healthcare**: Medical article summaries, FDA guidance assistants, and regulatory document analysis
* **Legal**: Contract review, compliance checking, and regulatory document processing
* **Retail and e-commerce**: Product descriptions, competitive analysis, and SEO optimization

**Function-based templates** are templates organized by business function:

* **Marketing**: Campaign briefs, content creation, social media posts, and brand messaging
* **Sales**: Outbound emails, prospect research, lead follow-ups, and client relationship management
* **Customer Support**: Help center Q\&A, review responses, and sentiment analysis
* **Content Creation**: Blog outlines, thought leadership articles, case studies, and presentation slides
* **Operations**: Meeting summaries, document processing, and workflow automation

**Custom Agents** are organization-specific templates from your Agent Library. These are specialized agents that other users within your organization have created and published, offering tailored solutions and workflows that align with your company's specific needs and processes.

### Filtering and discovery

You can filter templates by industry, function, and task.

* **Industry**: Browse sector-specific templates across Finance, Healthcare, Legal, Marketing, Retail, and more
* **Function**: Filter by business area such as Customer Support, Sales, Content Creation, or Operations
* **Task**: Find templates by specific capabilities like Analyzing, Brainstorming, Researching, Summarizing, or Generating

Each template includes a detailed preview and description of its capabilities so that you can select the best starting point for your agent development project.

### Agent Builder templates

The majority of prebuilt agent templates are [no-code agent](/no-code/introduction) templates.

The following prebuilt templates use [Agent Builder](/agent-builder/overview):

* **Content lifecycle**: Transform a single campaign brief into a fully built, ready-to-launch campaign. This agent extracts goals and summaries, generates project tasks, creates copy and visuals, and runs the final content through customizable legal checks, streamlining every step of the marketing content supply chain.
* **SDR outreach**: Generate personalized outreach emails tailored to a target account. This agent combines prospect research with product positioning to craft a multi-email sequence that aligns with the prospect's business priorities and challenges.


# Encryption Key Management
Source: https://dev.writer.com/home/encryption-key-management

Control encryption keys for data at rest with Writer-managed or customer-managed (BYOK) encryption across your organization.

This page covers how Writer encrypts data at rest and how organization admins can control encryption keys, manage scopes, and connect an external KMS for bring-your-own-key (BYOK) encryption.

<Info>
  Encryption is available on Enterprise plans. Organization admins can manage
  encryption settings in AI Studio > Admin Settings > Encryption.
</Info>

## How encryption works

Writer uses classic envelope encryption with **AES-256-GCM**. A Data Encryption Key (DEK) encrypts your data, and a Key Encryption Key (KEK)—the master key in KMS—wraps the DEK. The KEK never encrypts application data directly. Depending on enrollment, the KEK is stored in Writer's KMS or your own.

Encryption requests flow through the following steps:

1. **Data write**: Writer services send data to the EKM service for encryption
2. **DEK generation**: The EKM service generates a unique DEK for the scope
3. **Key wrapping**: The DEK is sent to the KMS (Writer's or yours) where the KEK wraps it, producing an encrypted DEK (eDEK)
4. **Storage**: Writer encrypts the data with the DEK using AES-256-GCM and stores the eDEK alongside the encrypted data
5. **Data read**: DEK upon retrieval is cached in memory with a five-minute TTL; when the DEK is not in cache, Writer unwraps the eDEK through the KMS and decrypts; when cached, Writer decrypts without contacting the KMS; Writer clears cached DEKs immediately on pause, revoke, rotate, or when the TTL ends

```mermaid theme={null} theme={null}
sequenceDiagram
    participant WS as Writer services
    participant EKM as EKM Service (Writer)
    participant KMS as KMS

    WS->>EKM: Write data
    EKM->>EKM: Generate DEK for scope
    EKM->>KMS: Wrap DEK with KEK
    KMS-->>EKM: Return eDEK
    EKM->>EKM: Encrypt with DEK (AES-256-GCM), store eDEK

    WS->>EKM: Read data
    EKM->>KMS: Unwrap eDEK with KEK
    KMS-->>EKM: Return DEK
    EKM-->>WS: Decrypt and return data
    Note over EKM: DEK cached in memory for five-minute TTL. Evicted on pause, revoke, rotate, or expiry.
```

## Supported BYOK providers

Writer supports BYOK with the following KMS providers:

| Provider  | Service                                                                 |
| --------- | ----------------------------------------------------------------------- |
| **AWS**   | [AWS KMS](https://aws.amazon.com/kms/)                                  |
| **Azure** | [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault) |
| **GCP**   | [GCP Cloud KMS](https://cloud.google.com/kms/docs)                      |

## Encryption tiers

Default encryption is included on all plans. Writer-managed and BYOK tiers are available on Enterprise plans at no additional cost.

|                      | Default (all plans)            | Writer-managed (Enterprise)                                           | BYOK (Enterprise)                     |
| -------------------- | ------------------------------ | --------------------------------------------------------------------- | ------------------------------------- |
| **What's encrypted** | Agent credentials, MCP secrets | All default-tier data plus Writer Agent files, messages, and sessions | Same as Writer-managed                |
| **KEK ownership**    | Writer                         | Writer                                                                | Customer                              |
| **KEK storage**      | Writer KMS                     | Writer KMS (AWS or GCP)                                               | Customer KMS (AWS, Azure, or GCP)     |
| **Revocation**       | Not available                  | DEK: self-service; KEK: not available                                 | Fully self-service (DEK and KEK)      |
| **Audit trail**      | Writer audit logs              | Writer audit logs                                                     | Writer audit logs + customer KMS logs |

<Note>
  Encryption currently covers Writer Agent and Connectors. Playbooks, Skills,
  custom agents, and no-code agents are not yet covered.
</Note>

## Scopes

Writer organizes encrypted data into hierarchical scopes, each with its own DEK:

* **Organization**: Covers all encrypted data in your organization.
  * **Team**: Covers all Writer Agent data for a single team.
    * **User**: Covers a user's Writer Agent sessions within a team, including requests, responses, and reasoning output (computer use). Users in multiple teams have a separate user scope in each team.
    * **Documents**: Covers user-uploaded files and Writer Agent deliverables.
  * **Connectors**: Covers credentials for each connector instance, so you can pause or revoke access to individual connectors.
  * **Agent Vault**: Covers agent credentials and secrets.

Actions on a parent scope cascade to all child scopes within it. This granular control means you can pause or revoke access to a single team, user, or connector without affecting the rest of your organization.

<Note>
  For Writer-managed encryption, organization-scope revocation requires
  [contacting support](mailto:support@writer.com) to prevent accidental data
  loss. With BYOK, all scope levels are fully self-service.
</Note>

## Key actions

Organization admins can perform the following actions from the encryption page in AI Studio.

| Action     | Effect                                                                                                                                                  | Reversible |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| **Pause**  | All decryption requests fail immediately, making data temporarily inaccessible until the key is re-enabled. The key and all wrapped DEKs remain intact. | Yes        |
| **Resume** | Restores access to a paused scope.                                                                                                                      | N/A        |
| **Revoke** | Decryption fails permanently. Depending on your KMS, revocation may mean disabling or scheduling deletion.                                              | No         |
| **Rotate** | Creates a new KEK version and re-wraps all DEKs under it. The old key can be retained for legacy data or decommissioned per your policy. No downtime.   | N/A        |

<Warning>
  Revoking a key is irreversible. All data encrypted under that scope becomes
  permanently inaccessible.
</Warning>

## BYOK controls

With BYOK, your customer-managed key (CMK) acts as the KEK and never leaves your cloud provider. Beyond scope-level actions, you can control Writer's access to all encrypted data directly from your KMS console:

* **Disable your CMK**: All decrypt operations across your organization fail immediately. Re-enabling the key restores access.
* **Delete your CMK**: All decrypt operations fail permanently. This is irreversible.

For step-by-step setup instructions, see [Set up BYOK encryption](/home/setup-byok).

<Warning>
  Disabling or deleting your CMK affects all encrypted data across your entire
  organization, not just a single scope.
</Warning>

## Next steps

* [Set up BYOK encryption](/home/setup-byok) to connect your own KMS
* [Configure guardrails](/home/guardrails) to enforce content safety and compliance policies
* [Configure plugins](/home/plugins-overview) to stream telemetry and security data to your monitoring platforms


# Event logs
Source: https://dev.writer.com/home/event-logs

View, filter, search, and export request-level logs to debug issues and audit agent activity in AI Studio.

The Events page in AI Studio captures every LLM request across your organization, giving you a searchable, filterable log of all platform activity. Use it to debug failed requests, audit model usage, and understand how your agents interact with LLMs.

<Info>
  The Events page is only available to organization admins, IT admins, and AI
  Studio full access roles.
</Info>

<Note>
  Event logs are available for [no-code and prebuilt
  agents](/no-code/introduction), [Agent Builder
  agents](/agent-builder/overview), and
  [API](/api-reference/api-keys)/[SDK](/home/sdks) usage. Event logs are not yet
  available for WRITER Agent.
</Note>

## Access the Events page

To open the Events page, [log in to AI Studio](https://app.writer.com/aistudio), select **Billing & reporting** in the left sidebar, then select **Observability**.

<img alt="The Events page in AI Studio showing summary metrics, a request volume chart, and an events table" />

Use the time range controls to select a preset window or define a custom start and end time. The summary metrics, chart, and events table all update to reflect the selected range.

## Find specific events

The events table lists every request with its timestamp, HTTP status code, model, session ID, who created it, and latency.

To narrow the table to the events you need, use any combination of the following:

* **Filters**—narrow by status (success or failure), model, or the user that created the request. Combine filters to isolate specific scenarios, such as all failed requests for a particular model.
* **Search**—search by exact request ID or session ID to locate a specific event.
* **Time range**—adjust the time window to focus on a specific period of activity.

<Note>
  The **Created by** field displays a name only when the completion request is
  sent by an Agent Builder or no-code application. Requests sent through the
  public API display **N/A** for this field.
</Note>

## Export event logs to CSV

Download event data as a CSV file for offline analysis, audits, or sharing with your team. The export reflects the events in the events table for your current view.

To export events, select the download button to the right of the search and filter controls above the events table.

<img alt="Download button on the Observability page, to the right of the Status and Model filters" />

The downloaded file follows these rules:

* **Time range**—the export includes only events within the time range currently selected on the Events page.
* **Masking**—if [content masking](#mask-sensitive-content) is enabled, request and response columns are hidden in the export, matching what you see in the event detail panel.

## Inspect an event

Select any row in the events table to open the detail panel. The panel has three tabs:

* **Details**—session and request IDs, timestamp, latency, status, request type, cache hit status, token usage and cost, model and provider, and runtime (start time, end time, duration).
* **Request**—the full JSON request payload.
* **Response**—the full JSON response payload.

Use the **Request** and **Response** tabs together to debug unexpected model behavior, and the **Details** tab to understand cost, performance, and routing for any individual request.

<Note>
  If prompts and responses appear as empty values,
  [masking](#mask-sensitive-content) is enabled.
</Note>

## Mask sensitive content

Event logs can capture sensitive information from user prompts and LLM responses. To protect this data, content masking hides these fields in the event detail panel, where they appear as empty values. Masking is enabled by default.

To configure masking, select the **Configure masking** control on the Events page. Choose one of the following options and select **Confirm**:

* **Enable masking**—user prompts and LLM responses are hidden. These fields appear as empty values in event logs.
* **Disable masking**—user prompts and LLM responses are fully visible in event logs.

<Note>
  Masking also applies to [CSV exports](#export-event-logs-to-csv) and data
  forwarded through [observability plugins](/home/plugins-overview), such as
  [Datadog Logs](/home/plugins/datadog-logs).
</Note>

## Next steps

* [Agent observability](/home/agent-observability): Drill into session logs and performance metrics for individual agents
* [Usage and spend](/home/usage-and-spend): Track organization-wide costs and token consumption
* [Configure plugins](/home/plugins-overview): Stream event data to external platforms like Datadog


# Add external models
Source: https://dev.writer.com/home/external-models

Configure external AI models from providers like AWS Bedrock or Microsoft Azure to use alongside Palmyra models in AI Studio

This guide covers how to add and manage external AI models in AI Studio. After configuring external models, your organization can use models from providers like AWS Bedrock and Microsoft Azure (Azure OpenAI) alongside Writer's Palmyra models when building agents.

<Note>
  External models are available on enterprise plans. Org admins, IT admins, and
  users with AI Studio full access roles can add and manage models. External
  model usage is billed at your provider's rate card plus a 10% WRITER
  pass-through fee.
</Note>

<Warning>
  AI Studio supports **text generation** and **embedding** models from external
  providers. Video, audio, and image generation models are not supported.
</Warning>

## How external models work

The [Models page in AI Studio](https://app.writer.com/aistudio) provides a unified governance layer for managing AI models across your organization. You can add models from external providers and control which teams have access to use them when building agents.

<Tip>
  The Models page also displays Writer's Palmyra models. Palmyra models are
  always available to all teams and don't require configuration. Select any
  Palmyra model to view its health status and details.
</Tip>

<img alt="" />

External model requests flow through the following steps:

1. **Credential configuration**: Administrators add provider credentials (API keys or IAM roles) to AI Studio
2. **Model selection**: Administrators choose which models from the provider to enable
3. **Access control**: Administrators assign model access to all teams or specific teams
4. **Agent building**: Developers with access can select the model when building agents
5. **Request routing**: When agents run, requests route through the configured provider credentials

```mermaid theme={null}
sequenceDiagram
    participant Builder as Agent Builder
    participant AIS as AI Studio
    participant Creds as Credential Store
    participant Provider as External Provider<br/>(AWS Bedrock, Microsoft Azure, etc.)

    Builder->>AIS: Select model for agent
    AIS->>Creds: Retrieve provider credentials
    Creds-->>AIS: Return credentials
    AIS->>Provider: Forward request with auth
    Provider-->>AIS: Return model response
    AIS-->>Builder: Deliver response to agent
```

## Available providers

AI Studio supports external models from the following providers. Select a provider to view detailed configuration instructions.

| Provider                                      | Status      | Auth type                              | Provider docs                                                                                                        |
| --------------------------------------------- | ----------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| [AWS Bedrock](/providers/aws-bedrock)         | Available   | Access keys or IAM Role ARN            | [View docs](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)                                  |
| [Microsoft Azure](/providers/microsoft-azure) | Available   | API key, endpoint URL, deployment name | [View docs](https://learn.microsoft.com/en-us/azure/foundry-classic/openai/how-to/create-resource?pivots=web-portal) |
| Baseten                                       | Coming soon | API key                                | -                                                                                                                    |

<Note>
  AWS Bedrock models are currently supported in the following regions:
  `us-east-1`, `us-west-1`, `us-west-2`, and `eu-west-1`. Additional regions may
  be added in future releases.
</Note>

## Add an external model

Add external models in [AI Studio](https://app.writer.com/aistudio) under **Models & Guardrails > Models**.

### Add a model in AI Studio

For reusability and easier rotation, [create named credentials](#create-credentials-from-the-llm-credentials-page) before adding a model. You can also enter credentials inline during the Add Model flow (see [Enter credentials directly](#enter-credentials-directly-when-adding-a-model)).

1. Navigate to **Models & Guardrails > Models** in [AI Studio](https://app.writer.com/aistudio)
2. Select **+ Add model**
3. Choose your provider (for example, AWS Bedrock or Microsoft Azure)
4. Enter your credentials
5. Choose which models to enable from the available list
6. Configure team access (all teams or specific teams)
7. Select **Add model** to complete the setup

<Tip>
  You can reuse credentials across multiple models. See [Manage
  credentials](#manage-credentials) for details.
</Tip>

<img alt="" />

## Manage team access

Control which teams can use external models when building agents.

### Configure model availability

When adding a model, you can set access to:

* **All teams**: The model is immediately available to everyone with builder access
* **Specific teams**: Restrict the model to selected teams

### Update team access

To view or update which teams can access a model:

1. Navigate to **Models & Guardrails > Models** in AI Studio
2. View the current team access in the **Team Access** column
3. Select the menu icon and choose **Edit** to update team access

<Note>
  Credentials are configured at the model level, not the team level. Team access
  controls who can use the model, but all authorized users share the same
  underlying provider credentials. Team members with access can use the models
  but cannot view the credentials.
</Note>

## Monitor model health

AI Studio automatically monitors the health of your configured models every 5 minutes and displays their status in the Models list.

| Status        | Description                                                        |
| ------------- | ------------------------------------------------------------------ |
| **Healthy**   | The model is responding correctly and credentials are valid        |
| **Unhealthy** | The model is not responding. Check credentials and provider status |

### View model health details

Select any model in the Models list to view detailed health information:

* **Health status**: Current health state (Healthy or Unhealthy)
* **Last checked**: Timestamp of the most recent health check
* **Model details**: Provider, model name, and model ID
* **Team access**: Which teams can use this model

To manually refresh the health status, select the **Refresh** button in the model details panel. This immediately checks the model's availability instead of waiting for the next automatic check.

### Automatic recovery

When a model becomes unhealthy, AI Studio temporarily removes it from the available models pool. After a cooldown period, the system automatically retries the model. If the underlying issue is resolved, the model returns to a healthy status without any manual intervention.

You don't need to delete or reconfigure a model that shows an unhealthy status due to transient issues like temporary provider outages. The system handles recovery automatically.

### Troubleshoot persistent issues

If a model remains unhealthy, check the following:

* **Credential validity**: Credentials may have expired or been revoked. Update credentials if needed.
* **Provider status**: The provider service may be experiencing extended downtime. Check the provider's status page.
* **Regional availability**: The model may not be available in the configured region.
* **Permission changes**: IAM policies may have been modified. Verify your IAM user or role still has the required permissions. For Microsoft Azure, confirm API keys are still valid and deployments were not removed or renamed.
* **Model deprecation**: The provider may have deprecated the model. Check the provider's documentation for model availability.

## Manage credentials

AI Studio stores provider credentials as named credential sets that you can reuse across multiple models. You can manage credentials from the dedicated **LLM Credentials** page or create them when adding a model.

<img alt="" />

### Create credentials from the LLM Credentials page

To create credentials before adding models:

1. Navigate to **Models & Guardrails > LLM Credentials** in [AI Studio](https://app.writer.com/aistudio)
2. Select **Add credentials**
3. Enter a **Credential name** (for example, `dev-aws-credentials` or `prod-azure-openai`)
4. Select the **Provider** (for example, Bedrock or Microsoft Azure)
5. Enter the [provider-specific authentication details](#available-providers)
6. Select **Save**

<img alt="" />

### Enter credentials directly when adding a model

When adding a model, you can enter credentials directly in the Add Model form instead of selecting existing credentials. Credentials entered this way are stored with the model but are not saved as a named credential set for reuse with other models.

To create reusable credentials that you can share across multiple models, use the [LLM Credentials page](#create-credentials-from-the-llm-credentials-page) instead.

### Reuse credentials across models

When adding additional models from the same provider:

1. Select your existing credentials from the **Credentials name** dropdown
2. The stored authentication details are automatically applied
3. You don't need to re-enter access keys or other sensitive values

This approach simplifies management when you have multiple models from the same provider—update credentials in one place and all associated models use the updated values.

### Update credentials

How you update expired or rotated credentials depends on how you originally configured them:

**Named credentials** (created on the LLM Credentials page):

1. Navigate to **Models & Guardrails > LLM Credentials** in AI Studio
2. Locate the credential in the list
3. Select the menu icon and choose **Edit credentials**
4. Update the authentication details and save

All models using this credential automatically use the updated values.

**Inline credentials** (entered directly when adding a model):

1. Navigate to **Models & Guardrails > Models** in AI Studio
2. Locate the model in the list
3. Select the menu icon and choose **Edit**
4. Update the credential values and save

<Tip>
  For easier credential management, use named credentials from the LLM
  Credentials page. Named credentials can be updated in one place and
  automatically apply to all models using them.
</Tip>

### Delete credentials

To delete a named credential:

1. Navigate to **Models & Guardrails > LLM Credentials** in AI Studio
2. Locate the credential in the list
3. Select the menu icon and choose **Delete**

<Warning>
  Before deleting credentials, verify that no models are using them. Models
  using deleted credentials will fail to authenticate with the provider.
</Warning>

### Security best practices

Follow these practices when managing provider credentials:

* Use dedicated service accounts rather than personal credentials
* Rotate credentials on a regular schedule (for example, every 90 days)
* Use descriptive credential names that indicate environment or purpose (for example, `prod-bedrock-us-east`)
* For AWS, prefer IAM Role ARN over access keys when possible

### Edit a model

To update an external model's configuration, including credentials and team access:

1. Navigate to **Models & Guardrails > Models** in AI Studio
2. Locate the model in the list
3. Select the menu icon and choose **Edit**
4. Update the configuration and save

### Delete a model

To remove an external model from AI Studio:

1. Navigate to **Models & Guardrails > Models** in AI Studio
2. Locate the model in the list
3. Select the menu icon and choose **Delete**

<Warning>
  Before deleting a model, verify that no active agents depend on it. Agents
  that reference a deleted model will return a "model not found" error when they
  run. You'll need to update the agent to use a different model before it can
  run successfully.
</Warning>

## Use external models

Once you add an external model, it's available to use the same way as Palmyra models.

### Agent Builder and no-code apps

In [Agent Builder](/agent-builder/overview) and [no-code chat apps](/no-code/introduction), external models appear in the **Model** dropdown alongside Palmyra models. Select any model your team has access to.

### API usage

External models use the same Writer API as Palmyra models. Use the [List models](/api-reference/completion-api/list-models) endpoint to see all available models and their IDs, then pass the model ID to any endpoint that supports the `model` parameter.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.writer.com/v1/chat/completions \
    -H "Authorization: Bearer $WRITER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "anthropic.claude-3-sonnet-20240229-v1:0",
      "messages": [{"role": "user", "content": "Hello!"}]
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  client = Writer()

  response = client.chat.chat(
      model="anthropic.claude-3-sonnet-20240229-v1:0",
      messages=[{"role": "user", "content": "Hello!"}]
  )
  print(response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import Writer from "writer-sdk";

  const client = new Writer();

  const response = await client.chat.chat({
    model: "anthropic.claude-3-sonnet-20240229-v1:0",
    messages: [{ role: "user", content: "Hello!" }],
  });
  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

## Next steps

* [Configure AWS Bedrock](/providers/aws-bedrock): Set up AWS credentials and add Bedrock models
* [Configure Microsoft Azure](/providers/microsoft-azure): Set up Azure OpenAI and add Microsoft Azure models
* [Choose a model](/home/models-overview): Compare Palmyra models with external provider models
* [Palmyra models](/home/models): Explore Writer's Palmyra model capabilities


# Manage files
Source: https://dev.writer.com/home/files

Upload, download, list, and delete files with the File API. Use files in Knowledge Graphs and no-code agents.

The File API allows you to manage files in your account. You can upload, download, list, and delete files.

After you upload a file, you can use it to perform actions such as attaching it to a [Knowledge Graph](/home/knowledge-graph) or using it as an input in a [no-code agent](/home/applications).

This guide shows you how to perform the following actions:

* [Upload a file](#upload-a-file)
* [Get a file](#get-a-file)
* [List all files](#list-all-files)
* [Delete a file](#delete-a-file)

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Upload a file

**Endpoint**: `POST /v1/files`

<Info>A file persists in your account until you [delete it](#delete-a-file).</Info>

You can optionally provide a `graphId` query parameter to associate the uploaded file with a Knowledge Graph during upload.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST "https://api.writer.com/v1/files?graphId=<GRAPH_ID>" \
    --header "Content-Type: text/plain" \
    --header "Content-Disposition: attachment; filename=<FILE_NAME>" \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --data-binary "@<FILE_PATH>"

  # Example:
  #   <FILE_NAME> = test.txt
  #   <FILE_PATH> = /path/to/test.txt
  #   <GRAPH_ID> = 6029b226-1ee0-4239-a1b0-cdeebfa3ad5a (optional)
  ```

  ```python Python theme={null}
  from pathlib import Path
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  # Read the file contents
  file_path = Path("<FILE_PATH>")  # Replace with your file path, for example: /path/to/test.txt
  file_name = file_path.name  # Get the filename from the path

  file_ = client.files.upload(
    content=file_path.read_bytes(),  # Read the actual file contents
    content_disposition=f"attachment; filename={file_name}",
    content_type="text/plain"
  )

  print(file_.id)
  ```

  ```javascript JavaScript theme={null}
  import fs from 'fs';
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const file = await client.files.upload({
    content: fs.createReadStream("<FILE_PATH>"),  // Replace with your file path, for example: /path/to/test.txt
    "Content-Disposition": "attachment; filename=<FILE_NAME>",  // Replace with your file name, for example: test.txt
    "Content-Type": "text/plain"
  });

  console.log(file.id)
  ```
</CodeGroup>

### Query parameters

| Parameter | Type     | Description                                                                    |
| --------- | -------- | ------------------------------------------------------------------------------ |
| `graphId` | `string` | Optional. The UUID of the Knowledge Graph to associate the uploaded file with. |

### Request body

| Parameter             | Type     | Description                                                                                                                                                                                                                                                                  |
| --------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content`             | `string` | The content of the file. See the [Python SDK](https://github.com/writer/writer-python?tab=readme-ov-file#file-uploads) and the [JavaScript SDK](https://github.com/writer/writer-node?tab=readme-ov-file#file-uploads) for more details about how to pass the file contents. |
| `content_disposition` | `string` | The [content disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) of the file.                                                                                                                                                        |
| `content_type`        | `string` | The [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types) of the file. The file upload supports `txt`, `doc`, `docx`, `ppt`, `pptx`, `jpg`, `png`, `eml`, `html`, `pdf`, `srt`, `csv`, `xls`, and `xlsx` file extensions.            |

### Response format

| Field        | Type            | Description                                                |
| ------------ | --------------- | ---------------------------------------------------------- |
| `id`         | `string`        | The ID of the file.                                        |
| `created_at` | `string`        | The date and time the file was created in ISO 8601 format. |
| `name`       | `string`        | The name of the file.                                      |
| `graph_ids`  | `array[string]` | The IDs of the Knowledge Graphs the file is attached to.   |
| `status`     | `string`        | The processing status of the file.                         |

<Info>
  If you provide a `graphId` during upload, the file is associated with that Knowledge Graph. However, the `graph_ids` field in the upload response is an empty list. The association is visible in the `graph_ids` list when you retrieve the file using the [Get a file](#get-a-file) endpoint.
</Info>

```json theme={null}
{
  "id": "1862f090-a281-48f3-8838-26c1e78b605e",
  "created_at": "2024-06-24T12:34:56Z",
  "name": "test.txt",
  "graph_ids": [],
  "status": "in_progress"
}
```

## List all files

**Endpoint**: `GET /v1/files`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request GET "https://api.writer.com/v1/files" \
    --header "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()
  page = client.files.list()

  for file in page.data:
      print(file.id, file.name, file.graph_ids, file.status)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();
  const files = await client.files.list();

  for (const file of files.data) {
      console.log(file.id, file.name, file.graph_ids, file.status);
  }
  ```
</CodeGroup>

### Query parameters

In addition to the [pagination parameters](/api-reference/file-api/get-all-files), this endpoint supports the following query parameters:

| Parameter    | Type     | Description                                                                                      |
| ------------ | -------- | ------------------------------------------------------------------------------------------------ |
| `graph_id`   | `string` | Filter files by the graph they are attached to.                                                  |
| `status`     | `string` | Filter files by status.                                                                          |
| `file_types` | `string` | Filter files by extension type. Separate multiple values by commas. For example, `txt,pdf,docx`. |

<Warning>
  The `file_types` parameter is not yet available in the Python and Node SDKs. It will be available in version 2.3.0.
</Warning>

### Response format

The response has the following structure:

| Field               | Type            | Description                                                |
| ------------------- | --------------- | ---------------------------------------------------------- |
| `data`              | `array[object]` | An array of file objects.                                  |
| `data[].id`         | `string`        | The ID of the file.                                        |
| `data[].created_at` | `string`        | The date and time the file was created in ISO 8601 format. |
| `data[].name`       | `string`        | The name of the file.                                      |
| `data[].graph_ids`  | `array[string]` | The IDs of the Knowledge Graphs the file is attached to.   |
| `data[].status`     | `string`        | The status of the file.                                    |
| `has_more`          | `boolean`       | Whether there are more files to fetch.                     |
| `first_id`          | `string`        | The ID of the first file in the response.                  |
| `last_id`           | `string`        | The ID of the last file in the response.                   |

```json theme={null}
{
  "data": [
    {
      "id": "f1234-abcd-1234",
      "created_at": "2025-03-07T23:20:50.978908Z",
      "name": "my_file.pdf",
      "graph_ids": [],
      "status": "completed"
    },
    {
      "id": "1234-abcd-5678",
      "created_at": "2025-03-07T23:20:44.047604Z",
      "name": "my_second_file.pdf",
      "graph_ids": [],
      "status": "completed"
    }
  ],
  "has_more": false,
  "first_id": "f1234-abcd-1234",
  "last_id": "1234-abcd-5678"
}
```

## Get a file

**Endpoint**: `GET /v1/files/{fileId}`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request GET "https://api.writer.com/v1/files/<FILE_ID>" \
    --header "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()
  file = client.files.retrieve("<FILE_ID>")

  print(file.id, file.name, file.graph_ids, file.status)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();
  const file = await client.files.retrieve('<FILE_ID>');

  console.log(file.id, file.name, file.graph_ids, file.status)
  ```
</CodeGroup>

### Response format

```json theme={null}
{
  "id": "f1234-abcd-1234",
  "created_at": "2025-03-07T23:20:50.978908Z",
  "name": "test.txt",
  "graph_ids": [],
  "status": "completed"
}
```

## Delete a file

**Endpoint**: `DELETE /v1/files/{fileId}`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request DELETE "https://api.writer.com/v1/files/<FILE_ID>" \
    --header "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()
  file = client.files.delete("<FILE_ID>")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();
  const fileDeleteResponse = await client.files.delete('<FILE_ID>');
  ```
</CodeGroup>

### Response format

The response has the following structure:

```json theme={null}
{
  "id": "f1234-abcd-1234",
  "deleted": true
}
```


# Understand generative AI
Source: https://dev.writer.com/home/generative-ai-overview

Learn how generative AI works and how to leverage Writer's models, safety, and best practices for building AI-powered applications.

This guide explains how generative AI works and how to use it effectively with Writer's platform.

Writer's generative AI enables applications to create text, images, and other content based on patterns learned from training data. Unlike traditional programming where you write explicit instructions, Writer's AI learns to generate appropriate responses by understanding context, patterns, and relationships in data.

## How generative AI works

Generative AI operates through a multi-step process that transforms your input into meaningful output:

![Generative AI Overview](https://files.readme.io/265b781-Overview_of_the_generative_AI_workflow.png)

### Core components

**Prompts** are the instructions you give to the AI model. They can be short questions, detailed instructions, or examples of the desired output format. Learn effective [prompting techniques](/home/prompting) to get the results you want.

**Foundation models** are large neural networks trained on vast amounts of text data. Writer offers [multiple Palmyra models](/home/models) with different capabilities, from fast text generation to advanced reasoning and tool calling.

**Tokenization** breaks down your input into smaller units (tokens) that the model can process. Understanding [tokens and pricing](/home/tokens) helps you improve costs and performance.

**Neural network processing** is where the model analyzes patterns, context, and relationships to determine the most appropriate response based on its training.

**Safety filtering** checks generated content for harmful, biased, or inappropriate material. Writer includes [built-in safety features](/home/toxic-check) to ensure responsible AI usage.

## Writer's model capabilities

### Palmyra models

Writer's [Palmyra models](/home/models) provide different capabilities optimized for various use cases. [Palmyra X6](/home/models#palmyra-x6) is the default model for WRITER Agent and is built for long-horizon agentic work: sub-agents, grounding and retrieval, MCP tool use, playbooks, and multi-hour objectives without supervision. [Palmyra X5](/home/models#palmyra-x5) has a 1M token context window, adaptive reasoning, and speed and cost efficiency. It covers general-purpose, financial, medical, and creative use cases. [Palmyra X4](/home/models#palmyra-x4) is also available for complex tasks that require extended context.

### Specialized features

* **Tool calling**: Enable models to use [external tools and APIs](/agent-builder/tool-calling-intro) for enhanced functionality
* **Vision capabilities**: Process and analyze images alongside text using [chat with images](/home/chat-with-images)
* **Knowledge Graph integration**: Connect to your data sources for accurate, contextual responses using [Knowledge Graph](/home/knowledge-graph-concepts)
* **Streaming**: Get responses in real-time for better user experience with [streaming](/home/streaming)

## Best practices

### Prompt engineering

* **Be specific**: Provide clear, detailed instructions for what you want
* **Use examples**: Show the model the format or style you're looking for
* **Provide context**: Give relevant background information
* **Iterate and refine**: Test different approaches to improve results
* **Learn more**: Explore [prompting techniques](/home/prompting) for advanced strategies

### Safety and reliability

* **Review outputs**: Always check generated content before using it
* **Set boundaries**: Define what the AI should and shouldn't do
* **Monitor performance**: Track quality and adjust as needed
* **Handle errors gracefully**: Plan for cases where the AI doesn't perform as expected

### Performance optimization

* **Choose appropriate models**: Select the right [Palmyra model](/home/models) for your use case
* **Optimize prompts**: Use clear, concise language and follow [common prompting strategies](/home/prompting)
* **Manage context length**: Stay within [token limits](/home/tokens) for best performance
* **Use streaming**: Enable [streaming responses](/home/streaming) for real-time delivery
* **Fine-tune parameters**: Adjust [model parameters](/home/model-parameters) for optimal results

## Getting started with Writer

To begin using Writer's generative AI in your applications:

1. **Choose a model** from [Writer's available models](/home/models) that fits your needs
2. **Design effective prompts** using [prompting techniques](/home/prompting)
3. **Test and iterate** to improve performance
4. **Implement safety measures** using Writer's [built-in safety features](/home/toxic-check)
5. **Monitor and optimize** for ongoing success

## Next steps

* Start with the [API quickstart](/home/quickstart) to get up and running
* Explore [Writer's models](/home/models) to find the right fit for your use case
* Learn [prompting techniques](/home/prompting) for better results
* Understand [model parameters](/home/model-parameters) for fine-tuning
* See how to [integrate with your applications](/home/integrations-overview)
* Build [no-code agents](/home/applications) for rapid prototyping
* Connect your data with [Knowledge Graph](/home/knowledge-graph-concepts) for accurate responses


# Configure guardrails
Source: https://dev.writer.com/home/guardrails

Configure guardrails to monitor and filter AI agent inputs and outputs for safety and compliance

This page covers the technical architecture and configuration options for guardrails in AI Studio. Guardrails enable organizations to enforce content safety, PII protection, and compliance policies across AI agents by integrating with external guardrail providers like [AWS Bedrock Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) and [Azure AI Content Safety](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview).

For step-by-step instructions on configuring guardrails in the AI Studio interface, see [Setting up guardrails](https://support.writer.com/article/312-guardrails) in the Writer Help Center.

<Info>
  Guardrails are available on Enterprise plans. Org admins and IT admins can
  configure guardrails in AI Studio.
</Info>

## How guardrails work

AI Studio's guardrail system sits between your agents and LLM calls, intercepting requests at configurable points to check content against your configured guardrail providers. This architecture provides centralized safety controls and compliance enforcement for all AI interactions.

<Note>
  For now, guardrails used with the API, SDK, and Agent Builder will only apply
  to external provider models.
</Note>

Guardrails can check content at three points in the request lifecycle:

* **Pre-call check**: Checks **user input** before the LLM call. Use this to block sensitive data users might accidentally submit (credit cards, Social Security numbers), stop prompt injection attacks, or filter inappropriate prompts. If content is blocked, the LLM is never called, saving costs.

* **Post-call check**: Checks **LLM output** before returning it to the user. Use this to catch sensitive data the LLM might generate in its response, filter inappropriate or off-topic content, or verify compliance of generated text. Even if user input is clean, the LLM might hallucinate PII or generate policy-violating content.

* **During-call check**: Checks content **in parallel** with the LLM call. Use this when you want to check input but can't afford the latency of waiting for a pre-call check to complete before starting the LLM call. The check runs concurrently, and the response is held until both the LLM and guardrail finish.

When a guardrail detects a violation, the request is halted and an error is returned to the agent.

<Note>
  For no-code chat apps, guardrails evaluate only the last message in the
  conversation, not the full conversation history.
</Note>

```mermaid theme={null}
sequenceDiagram
    participant Agent as AI Agent
    participant Gateway as AI Studio Gateway
    participant Guardrail as Guardrail Provider<br/>(AWS Bedrock, Azure Content Safety)
    participant LLM as LLM

    Agent->>Gateway: Send request

    Note over Gateway,Guardrail: Pre-call check (optional)
    Gateway->>Guardrail: Check input
    Guardrail-->>Gateway: Allow / Block

    Gateway->>LLM: Forward request

    Note over Gateway,Guardrail: During-call check (optional)
    Gateway->>Guardrail: Check in parallel

    LLM-->>Gateway: Return response
    Guardrail-->>Gateway: Allow / Block

    Note over Gateway,Guardrail: Post-call check (optional)
    Gateway->>Guardrail: Check output
    Guardrail-->>Gateway: Allow / Block

    Gateway-->>Agent: Return response
```

## Supported guardrail providers

AI Studio integrates with the following guardrail providers:

### AWS Bedrock Guardrails

[AWS Bedrock Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) is Amazon's content filtering and PII detection service. It provides content filters, denied topic detection, word filters, sensitive information filters, and contextual grounding checks.

For detailed setup instructions, see [Set up Bedrock guardrails](/home/guardrails-bedrock).

### Azure AI Content Safety

[Azure AI Content Safety](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview) is Microsoft's content moderation service. AI Studio supports two Azure Content Safety guardrail types:

* **Text Moderation**: Analyzes text for harmful content categories (Hate, Sexual, SelfHarm, Violence) with configurable severity thresholds.
* **Prompt Shields**: Detects prompt injection attacks in user inputs and malicious content embedded in documents.

For detailed setup instructions, see [Set up Azure Content Safety guardrails](/home/guardrails-azure).

## Guardrail modes

Select one or more modes when configuring a guardrail. You can combine modes; for example, use `pre_call` to check user input AND `post_call` to check LLM output for comprehensive protection.

| Mode          | What it checks   | When to use                                                                                         |
| ------------- | ---------------- | --------------------------------------------------------------------------------------------------- |
| `pre_call`    | User input       | Block PII users submit, prevent [prompt injection](/home/prompt_injections), filter harmful prompts |
| `post_call`   | LLM output       | Catch PII the LLM generates, filter inappropriate responses, verify compliance                      |
| `during_call` | Input (parallel) | Same as pre-call but with lower latency for time-sensitive apps                                     |

### Pre-call mode

Executes synchronously **before** the LLM call. If content is blocked, the LLM is never called.

* Checks user input only
* Saves LLM costs when blocking bad requests
* Adds latency before the LLM call starts

### Post-call mode

Executes synchronously **after** the LLM generates a response.

* Checks LLM output only
* Required for catching issues in LLM-generated content (the LLM might generate PII even if the input was clean)
* Adds latency after LLM processing

### During-call mode

Executes **in parallel** with the LLM call. The response is held until both the LLM and guardrail check complete.

* Checks input while the LLM processes the request
* Lower latency than pre-call if the guardrail check finishes before the LLM
* Useful for [streaming](/home/streaming) responses where latency matters

## Team scoping

By default, a guardrail applies to all teams in your organization. You can restrict a guardrail to specific teams if different teams have different compliance requirements. For example:

* A **healthcare team** might need strict HIPAA-compliant PII blocking
* A **marketing team** might only need basic content moderation
* An **internal tools team** might not need guardrails at all

When a guardrail is scoped to specific teams, requests from users on other teams skip that guardrail entirely.

| Scope setting       | Behavior                                                   |
| ------------------- | ---------------------------------------------------------- |
| All teams (default) | Guardrail runs for all requests in the organization        |
| Selected teams      | Guardrail only runs for requests from users on those teams |

When you create a new guardrail, it automatically applies to all agents within its configured scope—no agent reconfiguration is needed. If you delete a guardrail, agents continue to function normally but without that guardrail's protections.

## Error handling

When a guardrail blocks content, the system returns an error to the agent. The error includes:

* The guardrail name that triggered the block
* The type of violation detected (for example, `CREDIT_CARD` or `US_SSN` for PII blocks)
* A message indicating content was blocked

```json theme={null}
{
  "tpe": "BadRequest",
  "errors": [
    {
      "description": "Content blocked by guardrail: PII detected",
      "key": "fail.guardrail.blocked",
      "extras": {
        "guardrail_name": "pii-filter",
        "entity_type": "CREDIT_CARD"
      }
    }
  ],
  "extras": {}
}
```

Agents should handle guardrail errors gracefully and provide appropriate feedback to users. For PII blocks, consider prompting users to remove sensitive information and retry.

For a complete list of guardrail error codes, see [Error codes](/api-reference/error-codes#guardrails).

## Best practices

### Defense in depth

Layer multiple guardrails for comprehensive protection:

1. **Input filtering** (Pre-call): Block malicious prompts and PII before LLM processing
2. **Output filtering** (Post-call): Catch issues in LLM-generated content

### Performance considerations

Guardrails add processing time to each request, so consider the latency impact when designing your guardrail strategy. This is especially important for real-time applications like chat interfaces where response time affects user experience.

* **Pre-call guardrails** add latency before the LLM call. However, when content is blocked, pre-call mode is actually faster overall because it prevents the LLM call entirely.
* **Post-call guardrails** add latency after the LLM response is received. The total request time is the LLM processing time plus the guardrail check time.
* **During-call guardrails** run in parallel with the LLM call but buffer (hold) the response until the guardrail check completes. This means if the guardrail check takes longer than the LLM call, the response is delayed until the check finishes. If the LLM call takes longer, there's minimal added latency since the check completes while waiting.

For latency-sensitive applications, consider using pre-call mode for input validation to block bad requests early and avoid unnecessary LLM calls.

### Streaming responses

Guardrail modes behave differently with streaming responses:

* **During-call**: Checks input in parallel with the LLM. If the check finishes before the LLM starts outputting, streaming begins immediately. If the check takes longer, streaming is delayed until the check completes—but output chunks aren't buffered.

* **Post-call**: Collects all response chunks, assembles the complete response, runs the guardrail check, then delivers the chunks. The user doesn't receive any chunks until the guardrail check completes, so streaming latency is higher than non-streaming.

For latency-sensitive streaming applications, prefer during-call mode for input validation over post-call output checking when possible.

### Provider rate limits

Rate limits depend on your guardrail provider:

* **AWS Bedrock Guardrails**: See [Bedrock quotas](https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html) for current limits. You can request quota increases through the AWS Service Quotas console.
* **Azure AI Content Safety**: See [Content Safety service limits](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview#service-limits) for current limits. To request a rate increase, [contact Microsoft](mailto:contentsafetysupport@microsoft.com).

## Next steps

* [Set up Bedrock guardrails](/home/guardrails-bedrock): Configure AWS Bedrock for content filtering and PII detection
* [Set up Azure Content Safety guardrails](/home/guardrails-azure): Configure Azure Content Safety for text moderation and prompt injection detection
* [Prevent prompt injections](/home/prompt_injections): Learn about prompt injection attacks and mitigation
* [Setting up guardrails](https://support.writer.com/article/312-guardrails): Step-by-step UI configuration in the Help Center


# Set up Azure Content Safety guardrails
Source: https://dev.writer.com/home/guardrails-azure

Configure Azure AI Content Safety for text moderation and prompt injection detection

This guide shows you how to configure [Azure AI Content Safety](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview) guardrails with AI Studio. After completing this setup, AI Studio can use Azure Content Safety to moderate harmful content and detect prompt injection attacks across your AI agents.

AI Studio supports two Azure Content Safety guardrail types:

* **Text Moderation**: Analyzes text for harmful content categories with configurable severity thresholds
* **Prompt Shields**: Detects prompt injection attacks in user inputs and malicious content embedded in documents

These are configured as separate guardrails in AI Studio, allowing you to enable one or both depending on your safety requirements.

For an overview of how guardrails work in AI Studio, see [Configure guardrails](/home/guardrails).

## Prerequisites

Before configuring Azure Content Safety guardrails in AI Studio, you need:

* An Azure account with an active subscription
* An Azure AI Content Safety resource deployed in a [supported region](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview#region-availability)
* The API key and endpoint for your Content Safety resource
* Enterprise plan access to AI Studio

## Create a content safety resource and get credentials

If you haven't created an Azure AI Content Safety resource yet, follow these steps:

1. Sign in to the [Azure portal](https://portal.azure.com)
2. Select **Create a resource** and search for **Content Safety**
3. Select **Azure AI Content Safety** and select **Create**
4. Configure your resource:
   * Select your **Subscription** and **Resource group**
   * Choose a [supported region](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview#region-availability)
   * Enter a unique **Name** for your resource
   * Select a **Pricing tier**
5. Select **Review + create**, then **Create**
6. After deployment completes, navigate to your resource and go to **Resource Management > Keys and Endpoint**
7. Copy one of the **Keys** (either KEY 1 or KEY 2) and the **Endpoint** URL (for example, `https://your-resource-name.cognitiveservices.azure.com/`) for use in [AI Studio](#configure-in-ai-studio)

For more information on creating and managing Azure Content Safety resources, see the [Azure Content Safety documentation](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview).

## Configure in AI Studio

When adding an Azure Content Safety guardrail in AI Studio, provide these parameters:

| Parameter | Description                                                                                               |
| --------- | --------------------------------------------------------------------------------------------------------- |
| API key   | Your Azure Content Safety API key (from the Azure portal)                                                 |
| API base  | Your Content Safety endpoint URL (for example, `https://your-resource-name.cognitiveservices.azure.com/`) |

**Text Moderation** guardrails also accept the following optional parameters:

| Parameter          | Description                                                                                                                                                                | Default                 |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| Categories         | The content categories to evaluate. Select one or more: **Hate**, **SelfHarm**, **Sexual**, **Violence**.                                                                  | All categories selected |
| Severity threshold | The minimum severity level (0–6) that triggers a block. Lower values are more strict. For example, a threshold of `2` blocks content with a severity score of 2 or higher. | `2`                     |

<Note>
  By default, a guardrail applies to all teams in your organization. You can restrict it to specific teams during configuration. See [Team scoping](/home/guardrails#team-scoping) for details.
</Note>

## Text Moderation

Azure Content Safety Text Moderation analyzes text for harmful content across four categories:

| Category  | Description                                                    |
| --------- | -------------------------------------------------------------- |
| Hate      | Discriminatory or prejudiced content targeting identity groups |
| Sexual    | Sexually explicit or suggestive content                        |
| Self-Harm | Content related to self-harm or suicide                        |
| Violence  | Violent, threatening, or graphic content                       |

### How Text Moderation works with AI Studio

When configured as a guardrail, Text Moderation checks text content against each category and blocks the request if any category's severity score meets or exceeds the configured threshold.

Text Moderation supports the following guardrail modes:

* **Pre-call**: Check user input before the LLM processes it. Blocks harmful prompts and saves LLM costs.
* **Post-call**: Check LLM output before returning it to the user. Catches harmful content the LLM might generate.

For more information on severity levels and content categories, see [Harm categories](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/concepts/harm-categories) in the Azure documentation.

## Prompt Shields

Azure Content Safety Prompt Shields detects prompt injection attacks—attempts by users or embedded documents to manipulate an AI model into bypassing its safety rules or instructions.

Prompt Shields detects two types of attacks:

| Attack type         | Description                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| User Prompt attacks | Malicious instructions in user input designed to override the model's system prompt or safety behaviors |
| Document attacks    | Harmful commands embedded in documents or external content that the model processes                     |

### How Prompt Shields works with AI Studio

When configured as a guardrail, Prompt Shields analyzes user prompts and any associated documents for injection attacks. If an attack is detected, AI Studio blocks the request.

Prompt Shields supports the following guardrail modes:

* **Pre-call**: Check user input and documents before the LLM call. Blocks prompt injection attempts and prevents the LLM from processing malicious content.
* **During-call**: Check input in parallel with the LLM call for lower latency.

For more information on prompt injection detection, see [Prompt Shields](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/concepts/jailbreak-detection) in the Azure documentation.

## Verify your setup

Before configuring Azure Content Safety in AI Studio, test your resource directly to confirm your API key and endpoint work.

### Test Text Moderation

Run this cURL command, replacing `<endpoint>` and `<your_api_key>` with your values:

```shell theme={null}
curl --location --request POST '<endpoint>/contentsafety/text:analyze?api-version=2024-09-01' \
--header 'Ocp-Apim-Subscription-Key: <your_api_key>' \
--header 'Content-Type: application/json' \
--data-raw '{
  "text": "This is a test message",
  "categories": ["Hate", "Sexual", "SelfHarm", "Violence"]
}'
```

A successful response returns severity scores for each category:

```json theme={null}
{
  "categoriesAnalysis": [
    { "category": "Hate", "severity": 0 },
    { "category": "SelfHarm", "severity": 0 },
    { "category": "Sexual", "severity": 0 },
    { "category": "Violence", "severity": 0 }
  ]
}
```

### Test Prompt Shields

Run this cURL command, replacing `<endpoint>` and `<your_api_key>` with your values:

```shell theme={null}
curl --location --request POST '<endpoint>/contentsafety/text:shieldPrompt?api-version=2024-09-01' \
--header 'Ocp-Apim-Subscription-Key: <your_api_key>' \
--header 'Content-Type: application/json' \
--data-raw '{
  "userPrompt": "This is a test message",
  "documents": ["This is a test document"]
}'
```

A successful response indicates whether an attack was detected:

```json theme={null}
{
  "userPromptAnalysis": { "attackDetected": false },
  "documentsAnalysis": [{ "attackDetected": false }]
}
```

If either command returns an error, check that your API key and endpoint are correct and that your Content Safety resource is deployed in a supported region.

## Next steps

* [Configure guardrails](/home/guardrails): Learn about guardrail modes, team scoping, and error handling
* [Track usage and spend](/home/observability): Monitor guardrail activity and usage
* [Azure AI Content Safety documentation](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview): Detailed Azure documentation
* [Azure Content Safety pricing](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/content-safety/): Understand Azure Content Safety costs


# Set up Bedrock guardrails
Source: https://dev.writer.com/home/guardrails-bedrock

Configure AWS Bedrock Guardrails for content filtering and sensitive data protection

This guide shows you how to configure [AWS Bedrock Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) with AI Studio. After completing this setup, AI Studio can use your Bedrock guardrails to filter content, detect PII, and enforce topic policies across your AI agents.

For an overview of how guardrails work in AI Studio, see [Configure guardrails](/home/guardrails).

## Prerequisites

Before configuring Bedrock guardrails in AI Studio, you need:

* An AWS account with access to Amazon Bedrock
* A Bedrock guardrail created in the AWS console
* AWS credentials with permission to invoke the guardrail
* Enterprise plan access to AI Studio

## Create a Bedrock guardrail in AWS

If you haven't created a Bedrock guardrail yet, follow these steps in the AWS console:

1. Navigate to **Amazon Bedrock > Guardrails** in the AWS console
2. Select **Create guardrail**
3. Configure your guardrail policies:
   * **Content filters**: Block harmful content categories (hate, insults, sexual, violence)
   * **Denied topics**: Define custom topics to block
   * **Word filters**: Block specific words or phrases
   * **Sensitive information filters**: Detect and block PII types
   * **Contextual grounding**: Check for hallucinations and relevance
4. Save your guardrail and note the **Guardrail ID** and **Version**

For detailed instructions, see [Create a guardrail](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-create.html) in the AWS documentation.

## Configuration parameters

Configure these parameters when adding a Bedrock guardrail in AI Studio:

### Required parameters

| Parameter               | Description                                                                                                                                                                                                                                                               |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `guardrailIdentifier`   | The unique ID of your Bedrock guardrail (found in the AWS console)                                                                                                                                                                                                        |
| `guardrailVersion`      | The version of your guardrail (`DRAFT` or a version number like `1`, `2`)                                                                                                                                                                                                 |
| `aws_region_name`       | AWS region where your guardrail is deployed. Define the guardrail in the same region where your inference requests are made. See [supported regions](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-cross-region-support.html) in the AWS documentation. |
| `aws_access_key_id`     | AWS access key ID for authentication                                                                                                                                                                                                                                      |
| `aws_secret_access_key` | AWS secret access key for authentication                                                                                                                                                                                                                                  |

### Optional parameters

| Parameter                      | Description                                                                                                                                                                                       |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disable_exception_on_block`   | When `true`, returns a modified response instead of raising an exception when content is blocked. Useful for chat interfaces where exceptions may disrupt the conversation flow. Default: `false` |
| `aws_session_token`            | Session token for temporary AWS credentials (required when using STS)                                                                                                                             |
| `aws_session_name`             | Name for the AWS session                                                                                                                                                                          |
| `aws_profile_name`             | AWS profile name for credential retrieval from `~/.aws/credentials`                                                                                                                               |
| `aws_role_name`                | IAM role name for cross-account access or role assumption                                                                                                                                         |
| `aws_web_identity_token`       | Web identity token for OIDC-based authentication                                                                                                                                                  |
| `aws_sts_endpoint`             | Custom AWS STS endpoint URL                                                                                                                                                                       |
| `aws_bedrock_runtime_endpoint` | Custom Bedrock runtime endpoint URL                                                                                                                                                               |

## AWS authentication options

AI Studio supports multiple [AWS authentication methods](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) for Bedrock guardrails.

### Access keys (recommended for getting started)

Use IAM user access keys for straightforward authentication:

```
aws_access_key_id: AKIAIOSFODNN7EXAMPLE
aws_secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
aws_region_name: us-east-1
```

### Temporary credentials (STS)

Use temporary credentials from [AWS Security Token Service](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html):

```
aws_access_key_id: ASIAXXX...
aws_secret_access_key: xxx...
aws_session_token: FwoGZXIvYXdzEBY...
aws_region_name: us-east-1
```

### IAM role assumption

Assume an IAM role for cross-account access or elevated permissions:

```
aws_access_key_id: AKIAIOSFODNN7EXAMPLE
aws_secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
aws_role_name: bedrock-guardrail-role
aws_region_name: us-east-1
```

### Web identity (OIDC)

Use OIDC tokens for container-based or Kubernetes deployments:

```
aws_web_identity_token: eyJhbGciOiJSUzI1NiIs...
aws_role_name: bedrock-guardrail-role
aws_region_name: us-east-1
```

## Required IAM permissions

The AWS credentials you provide must have permission to invoke your Bedrock guardrail. Create an IAM policy with the following permissions:

```json theme={null}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["bedrock:ApplyGuardrail"],
      "Resource": "arn:aws:bedrock:*:*:guardrail/*"
    }
  ]
}
```

For production environments, scope the resource ARN to your specific guardrail:

```json theme={null}
{
  "Resource": "arn:aws:bedrock:us-east-1:123456789012:guardrail/abc123def456"
}
```

## Guardrail versioning

Bedrock guardrails support versioning, allowing you to test changes before applying them to production:

| Version value  | Behavior                                     |
| -------------- | -------------------------------------------- |
| `DRAFT`        | Uses the current draft version (for testing) |
| `1`, `2`, etc. | Uses a specific published version            |

**Recommended workflow:**

1. Test guardrail changes using `DRAFT` version in a development environment
2. Publish a new version in the AWS console when satisfied
3. Update production AI Studio configuration to use the new version number

## Bedrock guardrail capabilities

AWS Bedrock Guardrails provide several content filtering capabilities:

### Content filters

Block content based on harmful categories with configurable thresholds:

* **Hate**: Discriminatory or prejudiced content
* **Insults**: Demeaning or offensive language
* **Sexual**: Sexually explicit content
* **Violence**: Violent or threatening content
* **Misconduct**: Content promoting illegal activities
* **Prompt attacks**: Attempts to manipulate the model

### Denied topics

Define custom topics that should be blocked. Useful for:

* Preventing discussion of competitors
* Blocking off-topic conversations
* Enforcing industry-specific restrictions

### Sensitive information filters

Detect and block PII types including:

* Names, addresses, phone numbers
* Email addresses, URLs
* Credit card numbers, bank accounts
* Social Security numbers (US)
* Driver's license numbers
* Passport numbers

### Word filters

Block specific words, phrases, or patterns. Supports:

* Exact word matching
* Profanity filters
* Custom blocked terms

## Error handling

When Bedrock blocks content, AI Studio returns an error to the agent. The default behavior raises an exception that halts the request.

Set `disable_exception_on_block: true` to return a modified response instead of raising an exception. This is useful for:

* Chat interfaces where exceptions disrupt the conversation
* Applications that need to handle blocks gracefully
* Scenarios where you want to show a custom message to users

## Troubleshooting

### Common errors

| Error                       | Cause                           | Solution                                                 |
| --------------------------- | ------------------------------- | -------------------------------------------------------- |
| `AccessDeniedException`     | Missing IAM permissions         | Verify your IAM policy includes `bedrock:ApplyGuardrail` |
| `ResourceNotFoundException` | Invalid guardrail ID or version | Check the guardrail ID and version in the AWS console    |
| `ValidationException`       | Invalid parameters              | Verify region name and other parameters are correct      |
| `ThrottlingException`       | Rate limit exceeded             | Implement retry logic or request a quota increase        |

### Verify your guardrail

Test your guardrail directly in the AWS console before configuring it in AI Studio:

1. Navigate to your guardrail in the AWS Bedrock console
2. Select **Test** to open the testing interface
3. Enter sample content that should trigger the guardrail
4. Verify the guardrail blocks or allows content as expected

## Next steps

* [Configure guardrails](/home/guardrails): Learn about guardrail modes and scoping
* [Usage and spend](/home/usage-and-spend): Monitor guardrail activity and usage
* [AWS Bedrock Guardrails documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html): Detailed AWS documentation
* [Create a guardrail in AWS](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-create.html): Step-by-step AWS console instructions
* [AWS Bedrock pricing](https://aws.amazon.com/bedrock/pricing/): Understand Bedrock guardrail costs


# Help Center
Source: https://dev.writer.com/home/help-center





# Work with inline citations in Knowledge Graph responses
Source: https://dev.writer.com/home/inline-citations



This guide shows you how to work with inline citations in Knowledge Graph responses. Inline citations show which specific sources support each part of the response, enabling you to verify information and trace claims back to their origins.

<CardGroup>
  <Card title="Response with inline citations">
    Below is an example of a response from a Knowledge Graph that includes inline citations:

    ```
    Acme Corp's flagship product line includes three
    main categories: industrial tools, consumer
    electronics, and automotive parts. The industrial
    tools division offers precision manufacturing
    equipment with advanced automation capabilities
    [Acme-Product-Catalog.pdf](a1b2c3d4-e5f6-7890-abcd-ef1234567890).
    ```
  </Card>

  <Card title="Corresponding references array">
    Below is the corresponding `references` object that contains a direct snippet from the source file that was used to support the response:

    ```json theme={null}
    {"files": [
      {
        "text": "Industrial Tools Division: Our precision manufacturing equipment features advanced automation capabilities with real-time monitoring systems that reduce operational downtime by up to 40% through predictive maintenance algorithms.",
        "fileId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "score": 0.95,
        "page": 12,
        "cite": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      }
    ]}
    ```
  </Card>
</CardGroup>

## Enable inline citations

To enable inline citations in Knowledge Graph responses, set `inline_citations: true` in your `query_config` parameter when making direct Knowledge Graph queries via the `/v1/graphs/question` endpoint.

<CodeGroup>
  ```bash cURL highlight={7-9} theme={null}
  curl --location --request POST 'https://api.writer.com/v1/graphs/question' \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header 'Content-Type: application/json' \
    --data-raw '{
      "graph_ids": ["<GRAPH_ID>"],
      "question": "What are the key features of our product?",
      "query_config": {
        "inline_citations": true
      }
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client
  client = Writer()

  # Query Knowledge Graph with inline citations enabled
  response = client.graphs.question(
      graph_ids=["<GRAPH_ID>"],
      question="What are the key features of our product?",
      query_config={
          "inline_citations": True
      }
  )

  print(f"Answer: {response.answer}")
  print(f"\nFile references: {response.references.files}")
  print(f"\nWeb references: {response.references.web}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client
  const client = new Writer();

  // Query Knowledge Graph with inline citations enabled
  const response = await client.graphs.question({
      graph_ids: ["<GRAPH_ID>"],
      question: "What are the key features of our product?",
      query_config: {
          inline_citations: true
      }
  });

  console.log(`Answer: ${response.answer}`);
  console.log(`\nFile references: ${response.references.files}`);
  console.log(`\nWeb references: ${response.references.web}`);
  ```
</CodeGroup>

## Citation format and correlation

### Citation format

Inline citations appear in the answer from the LLM as `[source_name](identifier)`, where:

* `source_name` is the display name of the source: either the filename for files, or the page title for web sources
* `identifier` is the unique identifier used to correlate with the references array

The identifier format depends on the source type:

* **File sources**: Uses the `cite` field value, for example, `[document.pdf](a1b2c3d4-e5f6-7890-abcd-ef1234567890)`
* **Web sources**: Uses the `url` field value, for example, `[Web Page Title](https://example.com/page)`

### Correlate citations with references

Inline citations correspond to entries in the `references` object. Correlation differs by source type:

* **File sources**: Citations use the `cite` field from the file reference object as their identifier
* **Web sources**: Citations use the `url` field from the web reference object as their identifier

**Example correlation**:

Here's an example showing how inline citations correlate with the references array for both file and web sources:

**Response text**:

```
Acme Corp's flagship product line includes three main categories: industrial tools, consumer electronics, and automotive parts.
The industrial tools division offers precision manufacturing equipment with advanced automation capabilities
[Acme-Product-Catalog.pdf](a1b2c3d4-e5f6-7890-abcd-ef1234567890). According to recent industry analysis,
smart manufacturing adoption has increased by 40% across similar companies [Industry Trends Report](https://example.com/industry-trends).
```

**References object**:

```json theme={null}
{
  "files": [
    {
      "text": "Industrial Tools Division: Our precision manufacturing equipment features advanced automation capabilities with real-time monitoring systems that reduce operational downtime by up to 40% through predictive maintenance algorithms.",
      "fileId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "score": 0.95,
      "page": 12,
      "cite": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    }
  ],
  "web": [
    {
      "text": "Smart manufacturing adoption rates have increased by 40% across mid-size manufacturing companies, driven by automation and IoT integration.",
      "url": "https://example.com/industry-trends",
      "title": "Industry Trends Report",
      "score": 0.88
    }
  ]
}
```

**Citation mapping**:

* Citation `[Acme-Product-Catalog.pdf](a1b2c3d4-e5f6-7890-abcd-ef1234567890)` uses the `cite` field from the file reference object
* Citation `[Industry Trends Report](https://example.com/industry-trends)` uses the `url` field from the web reference object

## Link citations to source text with SDKs

When working with inline citations in SDK responses, you can programmatically link citation IDs to their corresponding source text. This enables you to build applications that display citations with their source material.

### Extract and correlate citations

These code samples demonstrate how to extract inline citations from Knowledge Graph responses and correlate them with their source material. The code parses citation markers in the answer text (like `[filename](citation-id)`) and matches them to the corresponding source text in the references object. Using similar patterns, you can extract citations and build applications that display citations with their source material.

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer
  import re

  # Initialize the Writer client
  client = Writer()

  # Query Knowledge Graph with inline citations
  response = client.graphs.question(
      graph_ids=["<GRAPH_ID>"],
      question="What are the key features of our product?",
      query_config={
          "inline_citations": True
      }
  )

  def extract_citations_with_sources(answer_text, references):
      '''Extract citations from answer text and link them to source material.
      
      This function parses inline citations from Knowledge Graph responses and correlates
      them with their source material from the references object.
      
      Args:
          answer_text (str): The response text containing inline citations
          references (object): The references object containing files and web sources
          
      Returns:
          list: Array of citation objects with source text and metadata
      '''
      # Use regex to find all citations in the format [display_text](identifier)
      # This pattern matches markdown-style links like [filename](citation-id)
      citation_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
      citations = re.findall(citation_pattern, answer_text)
      
      citation_data = []
      
      # Process each citation found in the answer text
      for display_text, identifier in citations:
          # Initialize citation info structure
          citation_info = {
              "display_text": display_text,  # The text shown in the citation (e.g., filename)
              "identifier": identifier,  # The unique citation ID
              "source_text": None,  # The actual source text
              "source_type": None   # The source type: "file" or "web"
          }
          
          # Search through file references to find matching citation
          if references and hasattr(references, 'files') and references.files:
              for file_ref in references.files:
                  # Match citation ID with the 'cite' field in file references
                  if hasattr(file_ref, 'cite') and file_ref.cite == identifier:
                      citation_info["source_text"] = file_ref.text  # Extract source text snippet
                      citation_info["source_type"] = "file"  # Mark as file source
                      citation_info["file_id"] = file_ref.file_id  # Store file ID for reference
                      citation_info["page"] = file_ref.page  # Store page number if available
                      break
          
          # Search through web references to find matching citation
          if references and hasattr(references, 'web') and references.web:
              for web_ref in references.web:
                  # Match citation ID with the 'url' field in web references
                  if hasattr(web_ref, 'url') and web_ref.url == identifier:
                      citation_info["source_text"] = web_ref.text  # Extract source text snippet
                      citation_info["source_type"] = "web"  # Mark as web source
                      citation_info["url"] = web_ref.url  # Store URL for reference
                      citation_info["title"] = web_ref.title  # Store page title
                      break
          
          citation_data.append(citation_info)
      
      return citation_data

  # Extract citations with their source text
  # This processes the response and creates a structured list of citations with their source material
  citations = extract_citations_with_sources(response.answer, response.references)

  # Display citations with source material
  # Loop through each citation and print its details along with the source text
  for citation in citations:
      print(f"Citation: [{citation['display_text']}]({citation['identifier']})")
      print(f"Source type: {citation['source_type']}")
      print(f"Source text: {citation['source_text']}")
      
      # Show additional metadata based on source type
      if citation['source_type'] == 'file':
          print(f"File ID: {citation['file_id']}, Page: {citation['page']}")
      elif citation['source_type'] == 'web':
          print(f"URL: {citation['url']}, Title: {citation['title']}")
      
      print("-" * 50)  # Separator between citations
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client
  const client = new Writer();

  // Query Knowledge Graph with inline citations
  const response = await client.graphs.question({
      graph_ids: ["<GRAPH_ID>"],
      question: "What are the key features of our product?",
      query_config: {
          inline_citations: true
      }
  });

  function extractCitationsWithSources(answerText, references) {
      /**
       * Extract citations from answer text and link them to source material.
       * 
       * This function parses inline citations from Knowledge Graph responses and correlates
       * them with their source material from the references object.
       * 
       * @param {string} answerText - The response text containing inline citations
       * @param {object} references - The references object containing files and web sources
       * @returns {Array} Array of citation objects with source text and metadata
       */
      
      // Use regex to find all citations in the format [display_text](identifier)
      // This pattern matches markdown-style links like [filename](citation-id)
      const citationPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
      const citations = [];
      let match;
      
      // Process each citation found in the answer text
      while ((match = citationPattern.exec(answerText)) !== null) {
          const [fullMatch, displayText, identifier] = match;
          
          // Initialize citation info structure
          const citationInfo = {
              displayText: displayText,    // The text shown in the citation (e.g., filename)
              identifier: identifier,      // The unique citation ID
              sourceText: null,          // The actual source text
              sourceType: null           // The source type: "file" or "web"
          };
          
          // Search through file references to find matching citation
          if (references?.files) {
              for (const fileRef of references.files) {
                  // Match citation ID with the 'cite' field in file references
                  if (fileRef.cite === identifier) {
                      citationInfo.sourceText = fileRef.text;      // Extract source text snippet
                      citationInfo.sourceType = "file";            // Mark as file source
                      citationInfo.fileId = fileRef.fileId;        // Store file ID for reference
                      citationInfo.page = fileRef.page;            // Store page number if available
                      break;
                  }
              }
          }
          
          // Search through web references to find matching citation
          if (references?.web) {
              for (const webRef of references.web) {
                  // Match citation ID with the 'url' field in web references
                  if (webRef.url === identifier) {
                      citationInfo.sourceText = webRef.text;       // Extract source text snippet
                      citationInfo.sourceType = "web";            // Mark as web source
                      citationInfo.url = webRef.url;              // Store URL for reference
                      citationInfo.title = webRef.title;          // Store page title
                      break;
                  }
              }
          }
          
          citations.push(citationInfo);
      }
      
      return citations;
  }

  // Extract citations with their source text
  // This processes the response and creates a structured list of citations with their source material
  const citations = extractCitationsWithSources(response.answer, response.references);

  // Display citations with source material
  // Loop through each citation and print its details along with the source text
  citations.forEach(citation => {
      console.log(`Citation: [${citation.displayText}](${citation.identifier})`);
      console.log(`Source type: ${citation.sourceType}`);
      console.log(`Source text: ${citation.sourceText}`);
      
      // Show additional metadata based on source type
      if (citation.sourceType === 'file') {
          console.log(`File ID: ${citation.fileId}, Page: ${citation.page}`);
      } else if (citation.sourceType === 'web') {
          console.log(`URL: ${citation.url}, Title: ${citation.title}`);
      }
      
      console.log("-".repeat(50));  // Separator between citations
  });
  ```
</CodeGroup>

## Response format

Knowledge Graph responses include a `references` object with `files` and `web` arrays. The `files` array contains file reference objects for file-based sources, and the `web` array contains web reference objects for web-based sources.

### File sources

File sources appear in the `references.files` array.

| Field    | Type      | Description                                                                                  |
| -------- | --------- | -------------------------------------------------------------------------------------------- |
| `text`   | `string`  | The actual text snippet from the source document that supports the response                  |
| `fileId` | `string`  | Unique identifier for the source file                                                        |
| `score`  | `number`  | Internal score used during the retrieval process for ranking and selecting relevant snippets |
| `page`   | `integer` | Page number where the snippet was found in the source document                               |
| `cite`   | `string`  | Unique identifier used in inline citations within the response text                          |

### Web sources

Web sources appear in the `references.web` array.

| Field   | Type     | Description                                                                                  |
| ------- | -------- | -------------------------------------------------------------------------------------------- |
| `text`  | `string` | The exact text snippet from the web source that was used to support the response             |
| `url`   | `string` | The URL of the web page where this content was found                                         |
| `title` | `string` | The title of the web page where this content was found                                       |
| `score` | `number` | Internal score used during the retrieval process for ranking and selecting relevant snippets |

## Streaming responses

When using streaming responses with Knowledge Graph tools, the `references` object containing sources and citations appears in the last chunk of the stream. This means:

* **Content chunks**: The response text with inline citations is streamed progressively
* **Final chunk**: Contains the graph data with the complete `references` object
* **Citation processing**: You need to collect all chunks and process the final one to access source text

### Stream responses with SDKs

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client
  client = Writer()

  # Configure Knowledge Graph tool with inline citations
  tools = [{
      "type": "graph",
      "function": {
          "description": "Search company knowledge base",
          "graph_ids": ["<GRAPH_ID>"],
          "subqueries": True,
          "query_config": {
              "inline_citations": True
          }
      }
  }]

  messages = [{'role': 'user', 'content': 'What are the key features of our product?'}]

  # Stream the response
  stream = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto",
      stream=True
  )

  # Collect streaming content and references
  references = None

  for chunk in stream:
      if chunk.choices[0].delta.content is not None:
          # Stream content progressively
          content = chunk.choices[0].delta.content
          print(content, end='', flush=True)
      
      # Check for graph data in the message
      if hasattr(chunk.choices[0], 'message') and chunk.choices[0].message:
          if hasattr(chunk.choices[0].message, 'graph_data') and chunk.choices[0].message.graph_data:
              # References are in the graph_data.references object
              references = chunk.choices[0].message.graph_data.references

  print("\nFile references:", references.files)
  print("\nWeb references:", references.web)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client
  const client = new Writer();

  // Configure Knowledge Graph tool with inline citations
  const tools = [{
      type: "graph",
      function: {
          description: "Search company knowledge base",
          graph_ids: ["<GRAPH_ID>"],
          subqueries: true,
          query_config: {
              inline_citations: true
          }
      }
  }];

  const messages = [{ role: "user", content: "What are the key features of our product?" }];

  // Stream the response
  const stream = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      tools: tools,
      tool_choice: "auto",
      stream: true
  });

  // Collect streaming content and references
  let references = null;

  for await (const chunk of stream) {
      if (chunk.choices[0].delta.content) {
          // Stream content progressively
          const content = chunk.choices[0].delta.content;
          process.stdout.write(content);
      }
      
      // Check for graph data in the message
      if (chunk.choices[0].message && chunk.choices[0].message.graph_data) {
          // References are in the graph_data.references object
          references = chunk.choices[0].message.graph_data.references;
      }
  }

  console.log(`\nFile references: ${references.files}`);
  console.log(`\nWeb references: ${references.web}`);
  ```
</CodeGroup>

## Next steps

* Learn about [Knowledge Graph query configuration](/home/knowledge-graph-query-config) for tuning search parameters
* Explore [Knowledge Graph tools](/home/kg-chat) for chat completions
* Check out [direct Knowledge Graph queries](/home/kg-query) for programmatic access


# Explore integrations
Source: https://dev.writer.com/home/integrations-overview



This guide provides an overview of Writer's integrations with popular AI frameworks and platforms. After reading this guide, you can choose the right integration for your specific use case and development needs.

Writer integrates with several leading AI frameworks and cloud platforms to provide flexible deployment options and enhanced capabilities. Each integration offers unique benefits depending on your infrastructure, development preferences, and specific requirements.

## Compare integrations

| Integration               | Best for                    | Key benefits                                   |
| ------------------------- | --------------------------- | ---------------------------------------------- |
| **Amazon Bedrock**        | Enterprise AWS environments | Serverless scaling, AWS ecosystem integration  |
| **Amazon Strands Agents** | Multi-agent AI applications | Agent orchestration, durable sessions          |
| **LangChain**             | Complex AI workflows        | Tool integration, data source connections      |
| **Traceloop/OpenLLMetry** | Production monitoring       | Observability, debugging, performance tracking |
| **Instructor**            | Structured data extraction  | Pydantic validation, retry mechanisms          |

## Amazon Bedrock

[Amazon Bedrock](https://aws.amazon.com/bedrock/) is a fully managed service for building and scaling generative AI applications. Writer's Palmyra X5 and X4 models are available on Bedrock, enabling you to use AWS's infrastructure and ecosystem to build and scale your applications.

<Tabs>
  <Tab title="Key benefits">
    * **Serverless scaling**: automatic scaling based on demand
    * **AWS ecosystem integration**: integration with other AWS services
    * **Enterprise security**: built-in security and compliance features
    * **Cross-region inference**: distribute traffic across multiple AWS regions
  </Tab>

  <Tab title="Ideal use cases">
    * Large-scale enterprise applications
    * AWS-native development workflows
    * Applications requiring automatic scaling
    * Organizations with existing AWS infrastructure
  </Tab>
</Tabs>

<Card title="Get started" icon="rocket">
  [Learn how to use Writer with Amazon Bedrock](/home/integrations/bedrock)
</Card>

## Amazon Strands Agents

[Amazon Strands Agents](https://strandsagents.com/latest/) is a platform for building sophisticated multi-agent AI applications that require orchestration and collaboration.

Strands Agents SDK enables you to create AI applications using a model-driven approach with multiple agents working together. Writer models integrate with the Strands ecosystem.

<Tabs>
  <Tab title="Key benefits">
    * **Multi-agent orchestration**: coordinate multiple AI agents in complex workflows
    * **Durable session management**: maintain state across agent interactions
    * **Asynchronous support**: handle concurrent operations efficiently
    * **Model-driven approach**: define agent behavior through configuration
  </Tab>

  <Tab title="Ideal use cases">
    * Research and content creation pipelines
    * Customer service automation with multiple specialized agents
    * Complex decision-making workflows
    * Applications requiring agent collaboration
  </Tab>
</Tabs>

<Card title="Get started" icon="rocket">
  [Learn how to use Writer with Amazon Strands Agents](/home/integrations/strands)
</Card>

## LangChain

[LangChain](https://www.langchain.com/) provides a framework for developing applications powered by language models. Writer's integration allows you to use Writer's capabilities within the LangChain ecosystem alongside other tools and data sources.

<Tabs>
  <Tab title="Key benefits">
    * **Tool integration**: connect Writer with external tools and APIs
    * **Data source connections**: integrate with databases, files, and web sources
    * **Chain composition**: build complex workflows with multiple components
    * **Ecosystem compatibility**: works with the broader LangChain tool ecosystem
  </Tab>

  <Tab title="Ideal use cases">
    * Research assistants with web search capabilities
    * Document processing pipelines
    * Custom AI applications with multiple data sources
    * Applications requiring extensive tool integration
  </Tab>
</Tabs>

<Card title="Get started" icon="rocket">
  [Learn how to use Writer with LangChain](/home/integrations/langchain)
</Card>

## Traceloop/OpenLLMetry

[OpenLLMetry](https://openllmetry.com/) provides observability for LLM applications through OpenTelemetry-compatible tracing. It offers detailed insights into your Writer API usage, performance characteristics, and debugging capabilities.

<Tabs>
  <Tab title="Key benefits">
    * **Comprehensive observability**: track prompts, completions, and token usage
    * **OpenTelemetry compatibility**: works with any OpenTelemetry-compatible backend
    * **Performance insights**: monitor latency, throughput, and error rates
    * **Debugging capabilities**: trace issues across complex AI workflows
  </Tab>

  <Tab title="Ideal use cases">
    * Production AI applications
    * Applications requiring performance monitoring
    * Complex AI workflows needing debugging
    * Organizations with existing observability stacks
  </Tab>
</Tabs>

<Card title="Get started" icon="rocket">
  [Learn how to use Writer with OpenLLMetry](/home/integrations/openllmetry)
</Card>

## Instructor

[Instructor](https://github.com/instructor-ai/instructor) provides a high-level framework for getting structured outputs from language models. It offers built-in retry mechanisms, Pydantic validation, and prompt optimization specifically designed for structured data extraction.

<Tabs>
  <Tab title="Key benefits">
    * **Structured data extraction**: convert unstructured text into validated data structures
    * **Built-in retry logic**: automatic error handling and retry mechanisms
    * **Pydantic validation**: type-safe data validation and parsing
    * **Prompt optimization**: automatic prompt minimization and optimization
  </Tab>

  <Tab title="Ideal use cases">
    * Data extraction from documents and text
    * API response parsing and validation
    * Content analysis and classification
    * Data repair and cleaning workflows
  </Tab>
</Tabs>

<Card title="Get started" icon="rocket">
  [Learn how to use Writer with Instructor](/home/integrations/instructor)
</Card>

## Next steps

Once you've chosen an integration:

1. **Set up your development environment** with the required dependencies
2. **Follow the integration-specific guide** to implement Writer in your application
3. **Test your implementation** with sample data and workflows
4. **Deploy to production** following best practices for your chosen platform

For additional help, see the [API Quickstart](/home/quickstart) or explore the [SDKs documentation](/home/sdks) for language-specific implementation details.


# Invoke Palmyra models on Amazon Bedrock
Source: https://dev.writer.com/home/integrations/bedrock



Palmyra X5 and Palmyra X4 are available on [Amazon Bedrock](https://aws.amazon.com/bedrock/).

Amazon Bedrock is a fully managed service from AWS that enables developers to build and scale generative AI applications using foundation models from leading AI companies. This integration allows you to use Palmyra X5 and X4 in Amazon's serverless environment.

This guide covers how to:

* Install and configure the AWS CLI, which you need to connect to Bedrock
* List available Writer models on Bedrock
* Use the AWS `boto3` Python SDK to invoke Palmyra models on Bedrock

## Prerequisites

Before interacting with Palmyra models on Bedrock, you need an [AWS account](https://aws.amazon.com/) with access to Bedrock.

## Install and configure the AWS CLI

1. [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
2. Configure the AWS CLI with your AWS credentials. You can configure the CLI using the following command and providing your AWS account credentials:

```bash theme={null}
aws configure sso
```

Alternatively, you can configure the CLI with environment variables or a credentials file. See the [AWS CLI documentation](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html#getting-started-quickstart-config) for more details.

3. Test that your AWS CLI is configured correctly by running the following command:

```bash theme={null}
aws sts get-caller-identity
```

You should see output similar to the following:

```bash theme={null}
{
    "UserId": "A123456789012",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/your-username"
}
```

## Install the AWS boto3 SDK for Python

To invoke Palmyra models on Bedrock using Python, you can use the AWS `boto3` SDK.

Install the `boto3` SDK using the following command:

```bash theme={null}
pip install boto3
```

## Subscribe to Bedrock Models

<Warning>
  Writer Palmyra X5 and X4 models are available in Amazon Bedrock in the US West (Oregon) AWS Region with [cross-Region inference](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html). For the most up-to-date information on model support by Region, refer to the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html).
</Warning>

To subscribe to the Palmyra X5 and Palmyra X4 models on Bedrock, go to the [Bedrock console](https://console.aws.amazon.com/bedrock/home?region=us-west-2#/modelaccess) and request access to the Writer models you want to use.

You can also list the available models from Writer using the following command:

<CodeGroup>
  ```bash AWS CLI theme={null}
  aws bedrock list-foundation-models --region=us-west-2 --by-provider writer --query "modelSummaries[*].modelId"
  ```

  ```python boto3 (Python SDK) theme={null}
  import boto3

  bedrock = boto3.client(service_name="bedrock", region_name="us-west-2")
  response = bedrock.list_foundation_models(byProvider="writer")

  for summary in response["modelSummaries"]:
      print(summary["modelId"])
  ```
</CodeGroup>

## Use Palmyra X5 and Palmyra X4 on with the AWS Python SDK

<Warning>
  Palmyra X5 and Palmyra X4 are only available in the US West (Oregon) region but can be accessed through cross-Region inference. Cross-Region inference allows you to distribute traffic across multiple AWS Regions. To learn more about cross-Region inference for Bedrock, see [Increase throughput with cross-Region inference](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html) and [Supported Regions and models for inference profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html).
</Warning>

The following example uses the `boto3` Python SDK to create a chat completion with the Palmyra X5 model on Bedrock. The `modelId` is `us.writer.palmyra-x5-v1:0`, which is the ID for the [cross-region inference profile for Palmyra X5](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html).

```python boto3 (Python SDK) theme={null}
import boto3

client = boto3.client(service_name="bedrock-runtime", region_name="us-west-2")
response = client.converse(
    modelId="us.writer.palmyra-x5-v1:0",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "text": "Write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions"
                }
            ],
        }
    ],
)
print(response["output"]["message"]["content"][0]["text"])
```

Learn more about the `converse` method in the [boto3 Python SDK documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html).

## Example: Summarize a meeting transcript

The following example uses Palmyra X5 to summarize a meeting transcript and provide a list of action items from the transcript.

It takes a meeting transcript text file, passes it to Palmyra X5 with instructions to provide a summary of the meeting and a list of action items, and prints the response.

```python boto3 (Python SDK) theme={null}
import boto3
from botocore.exceptions import ClientError

# Create a Bedrock Runtime client in the AWS Region you want to use.
client = boto3.client("bedrock-runtime", region_name="us-west-2")

# Set the model ID, e.g. Palmyra X5.
# This is the ID for the cross-region inference profile for Palmyra X5.
# See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html.
model_id = "us.writer.palmyra-x5-v1:0"

transcript_file_path = "call-transcript.txt"
transcript_format = "txt"

# Load the document
with open(transcript_file_path, "rb") as file:
    document_bytes = file.read()

# Start a conversation with a user message and the document
conversation = [
    {
        "role": "user",
        "content": [
            {
                "text": "Create a summary of the meeting notes and provide a list of action items."
            },
            {
                "document": {
                    # Available formats: html, md, pdf, doc/docx, xls/xlsx, csv, and txt
                    "format": transcript_format,
                    "name": "Meeting Notes",
                    "source": {"bytes": document_bytes},
                }
            },
        ],
    }
]

try:
    # Send the message to the model.
    response = client.converse(
        modelId=model_id,
        messages=conversation,
        inferenceConfig={"maxTokens": 500, "temperature": 0.3},
    )

    # Extract and print the response text.
    response_text = response["output"]["message"]["content"][0]["text"]
    print(response_text)

except (ClientError, Exception) as e:
    print(f"ERROR: Can't invoke '{model_id}'. Reason: {e}")
    exit(1)
```

## Additional resources

See more information and examples below:

* [AWS blog post](https://aws.amazon.com/blogs/aws/writer-palmyra-x5-and-x4-foundation-models-are-now-available-in-amazon-bedrock/) about Palmyra X5 and X4 on Bedrock
* [Additional code examples for Amazon Bedrock](https://github.com/writer/aws-examples/tree/main/bedrock-examples)


# Structured output with Instructor
Source: https://dev.writer.com/home/integrations/instructor



This tutorial will guide you through using Writer with [Instructor](https://useinstructor.com/), a Python library that makes it easy to get structured data like JSON from LLMs.

## Prerequisites

* Python 3.8 or higher installed
* [Poetry](https://pypi.org/project/poetry/) installed (see their [installation guide](https://python-poetry.org/docs/#installation))
* A Writer API key (follow the [Quickstart](/home/quickstart) to obtain an API key)

## Getting started

To get started with Instructor, you'll need to install the library and set up your environment.

<Steps>
  <Step title="Obtain an API key">
    First, make sure that you've signed up for a Writer AI Studio account and obtained an API key. You can follow the [Quickstart](/home/quickstart) to obtain an API key.
  </Step>

  <Step title="Install instructor">
    Once you've done so, install `instructor` with Writer support by running:

    ```bash theme={null}
    pip install instructor[writer]
    ```
  </Step>

  <Step title="Set the `WRITER_API_KEY` environment variable">
    Make sure to set the `WRITER_API_KEY` environment variable with your Writer API key or pass it as an argument to the Writer constructor.
  </Step>
</Steps>

## Basic usage

Following is a simple example of how to use `instructor` with Writer:

```python theme={null}
import instructor
from writerai import Writer
from pydantic import BaseModel

# Initialize Writer client
client = instructor.from_writer(Writer(api_key="your API key"))


class User(BaseModel):
    name: str
    age: int


# Extract structured data
user = client.chat.completions.create(
    model="palmyra-x5",
    messages=[{"role": "user", "content": "Extract: John is 30 years old"}],
    response_model=User,
)

print(user)
#> name='John' age=30
```

This code creates a simple data model with two fields: `name` and `age`. It then uses the `instructor.from_writer` function to create a `client` object that uses the Writer API to extract structured data from a text.

## Instructor modes

Instructor supports two modes for structured output with Writer:

* [JSON mode](#json-mode): Returns a response that strictly follows the provided JSON schema.
* [Tools mode](#tools-mode): Returns a structured response using the `tool_calls` field, where it populates the defined tool and its arguments based on the input.

Both modes use the [Writer chat completion endpoint](/api-reference/completion-api/chat-completion) to generate structured outputs. The key difference lies in how the model returns the data: either as a raw JSON object or via a structured tool call.

### JSON mode

JSON mode uses Writer's native JSON schema support. When you use this mode:

* Instructor converts your Pydantic model into a JSON schema and passes it as the `response_format` parameter
* The model generates a response that strictly follows the provided JSON schema
* Instructor then parses the JSON response and validates it against your original Pydantic model

<Tip>
  For more details on structured outputs and JSON schema usage, see our [structured output documentation](/home/structured-output).
</Tip>

### Tools mode

Tools mode uses Writer's tool calling capabilities. When you use this mode:

* Instructor converts your Pydantic model into a tool definition
* Instructor adds this tool definition to the API call using the `tools` parameter
* The model returns a structured response using the `tool_calls` field, where it populates the defined tool and its arguments based on the input
* Instructor extracts the arguments from the `tool_calls` field and validates them against the original Pydantic model

<Tip>
  For more details on tool calling, see our [tool calling documentation](/home/tool-calling).
</Tip>

### Specifying the mode

You can specify which mode to use when initializing the Instructor client:

```python theme={null}
import instructor
from writerai import Writer

# Use JSON mode
client = instructor.from_writer(
    Writer(api_key="your API key"),
    mode=instructor.Mode.WRITER_JSON
)

# Use Tools mode
client = instructor.from_writer(
    Writer(api_key="your API key"),
    mode=instructor.Mode.WRITER_TOOLS
)

```

If you don't specify a mode, Instructor will use `WRITER_TOOLS` by default.

## Instructor vs. Writer SDK

When working with structured outputs, you can choose between two main approaches: using Instructor, a high-level framework built on top of LLM providers, or using the Writer SDK directly, which offers low-level access to the Writer LLMs. Each option comes with its own strengths and trade-offs, depending on your goals and level of control needed.

| Feature                      | Instructor                                                               | Writer SDK                                                 |
| ---------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------- |
| **Retry mechanism**          | Built-in retry logic with error handling and prompt editing              | Manual implementation required                             |
| **Schema handling**          | Auto-converts a wide range of Python types into JSON Schema via Pydantic | Manual schema conversion required                          |
| **Validation & reliability** | Enhanced outcomes through Pydantic validation + retries                  | Same results possible with manual implementation           |
| **Prompt optimization**      | Built-in prompt minimization and optimization                            | Manual prompt engineering required                         |
| **Model access**             | Limited to supported models (currently V4 + Tool Calls)                  | Full access to latest models (V4, V5) and all output modes |
| **Update speed**             | Requires updates/contributions for new features                          | Immediate support for latest Palmyra features              |

For more on Instructor’s advanced retry and error handling capabilities, see the [official Instructor retrying guide](https://python.useinstructor.com/concepts/retrying/).

## Building a data repair tool with Instructor and Writer

You can also use Instructor to do advanced data extraction and repair. In this example, you'll build a Python application that extracts structured data from text, CSV, and PDF files using Instructor and Writer. This application will:

* Parse text, CSV, and PDF files
* Extract and validate structured data using Instructor and Writer
* Output the results in CSV format

The finished code for this tutorial is available in the [API tutorials](https://github.com/writer/api-tutorials/tree/main/instructor-and-writer-tutorial) GitHub repository.

### Setting up the project

<Steps>
  <Step title="Create a new project">
    First, create a new project and set up Poetry for dependency management:

    ```bash theme={null}
    mkdir instructor-and-writer-tutorial
    cd instructor-and-writer-tutorial
    poetry init -y
    ```
  </Step>

  <Step title="Add dependencies">
    Add the required dependencies to your project:

    ```bash theme={null}
    poetry add instructor writer-sdk python-dotenv pydantic
    ```
  </Step>

  <Step title="Set up your environment variables">
    Create a `.env` file in your project root and add your Writer API key:

    ```
    WRITER_API_KEY=your_api_key_here
    ```
  </Step>

  <Step title="Create `main.py` file and add imports">
    Create a `main.py` file and add the following imports:

    ```python theme={null}
    import asyncio
    import csv
    import json
    import os
    from typing import Annotated, Type, Iterable, List

    import instructor
    from dotenv import load_dotenv
    from pydantic import BaseModel, AfterValidator, Field
    from writerai import Writer, AsyncWriter

    load_dotenv()
    ```

    Here's what each import is used for:

    * `asyncio`: This is used to run the application on multiple files concurrently.
    * `csv`: This is used to write the extracted data to a CSV file.
    * `json`: This is used to write the extracted data to a JSON file.
    * `os`: This is used to read the files.
    * `instructor`: The `instructor` library is used for structured output.
    * `writerai`: This is the Writer Python SDK, which is used to interact with the Writer API.
    * `typing` and `pydantic`: These modules are used to define the types for fields in the `UserExtract` class defined in the next step.
    * `dotenv`: The `dotenv` module is used to load the `.env` file that contains your Writer API key.
  </Step>

  <Step title="Setting up Writer client">
    Initialize the Writer client for both synchronous and asynchronous operations:

    ```python theme={null}
    writer_client = Writer()
    async_writer_client = AsyncWriter()
    ```
  </Step>
</Steps>

### Defining the data model

In order for Instructor to extract structured output, you need to define a data model using Pydantic. To define the data model, create a `UserExtract` class to represent the data you want to extract:

```python theme={null}
class UserExtract(BaseModel):
    @staticmethod
    def first_last_name_validator(v):
        if v[0] != v[0].upper() or v[1:] != v[1:].lower() or not v.isalpha():
            raise ValueError("Name must contain only letters and start with uppercase letter")
        return v

    first_name: Annotated[str, AfterValidator(first_last_name_validator)] = Field(
        ..., description="The name of the user"
    )
    last_name: Annotated[str, AfterValidator(first_last_name_validator)] = Field(
        ..., description="The surname of the user"
    )
    email: str
```

This data model defines the fields that you want to extract from the files. The `first_name` and `last_name` fields are validated to ensure they start with an uppercase letter and contain only letters. In this example, the `email` field is a simple string field, though you could also use a Pydantic field to validate the email format.

### Parsing the files

With the data model defined, you can now implement file parsing. This involves creating functions to open the files and extract the text.

<Steps>
  <Step title="Create a function to handle file processing">
    Implement the main file handler function that orchestrates the entire process:

    ```python theme={null}
    async def handle_file(file_path: str, response_model: Type[BaseModel], output_path: str = None) -> None:
        extension = os.path.splitext(file_path)[1]
        name = os.path.splitext(os.path.basename(file_path))[0]

        file_text = await fetch_file_text(file_path, name, extension)
        repaired_entities = await repair_data(file_text, response_model)

        print(f"Number of entities extracted from {name}{extension}: {len(repaired_entities)}")
        return generate_csv(repaired_entities, response_model, output_path)
    ```

    This function handles the file processing logic, including file type validation, text extraction, data repair, and CSV generation.
  </Step>

  <Step title="Create a function to read the files">
    Next, create a function to read the files based on the given path and extension:

    ```python theme={null}
    async def fetch_file_text(file_path: str, name: str, extension: str) -> str:
        allowed_extensions = [".txt", ".csv", ".pdf"]
        if extension not in allowed_extensions:
            raise ValueError(f"File extension {extension} is not allowed. Only {', '.join(allowed_extensions)}")

        print(f"Reading {name}{extension} content...")
        with open(file_path, 'rb') as file:
            file_contents = file.read()

        return await parse_file(file_contents, name, extension)
    ```
  </Step>

  <Step title="Extract the file content">
    Next, create a function to extract the text from the files. For text files, this function simply reads the file contents. For PDFs, the function uploads the PDF using Writer's [file upload endpoint](/api-reference/file-api/upload-files), parses the text using [PDF parsing tool](/api-reference/tool-api/pdf-parser), and then deletes the file from Writer's servers using the [file delete endpoint](/api-reference/file-api/delete-file):

    ```python theme={null}
    async def parse_file(file_bytes_content: bytes, name: str, extension: str) -> str:
        file_text = ""

        if extension == ".pdf":
            print(f"Uploading {name}{extension} content to writer servers...")
            file = await async_writer_client.files.upload(
                content=file_bytes_content,
                content_disposition=f"attachment; filename={name + extension}",
                content_type="application/octet-stream",
            )

            print(f"Converting {name}{extension} content from PDF to text...")
            file_text = await async_writer_client.tools.parse_pdf(
                file_id=file.id,
                format="text",
            )

            print(f"Deleting {name}{extension} from writer servers...")
            await async_writer_client.files.delete(file.id)
        else:
            print(f"Converting {name}{extension} content...")
            file_text = file_bytes_content.decode("utf-8")

        return file_text
    ```
  </Step>
</Steps>

### Extracting and repairing the data

With the file content extracted, you can now implement data extraction and repair using Instructor and Writer.

<Steps>
  <Step title="Create a function to repair the data">
    Create a function to extract and repair data using Instructor:

    ```python theme={null}
    async def repair_data(file_text: str, response_model: Type[BaseModel]) -> List[BaseModel]:
        instructor_client = instructor.from_writer(client=async_writer_client)

        if not issubclass(response_model, BaseModel):
            raise ValueError("Response model must be subclass of pydantic BaseModel")

        print("Extracting data featuring Instructor tools...")
        return await instructor_client.chat.completions.create(
            model="palmyra-x5",
            response_model=Iterable[response_model],
            max_retries=5,
            messages=[
                {"role": "user", "content": f"Extract entities from {file_text}"},
            ],
        )
    ```
  </Step>

  <Step title="Implementing CSV generation">
    Add a function to save the extracted data to CSV:

    ```python theme={null}
    def generate_csv(entities: List[BaseModel], response_model: Type[BaseModel], output_path: str = None) -> None:
        fieldnames = list(response_model.model_json_schema()["properties"].keys())
        file_path = f"{response_model.__name__}.csv"

        if output_path:
            file_path = output_path + file_path
            os.makedirs(os.path.dirname(file_path), exist_ok=True)

        with open(file_path, "w") as file:
            dict_writer = csv.DictWriter(file, fieldnames=fieldnames)
            dict_writer.writeheader()
            for entity in entities:
                dict_writer.writerow(json.loads(response_model(**entity.model_dump()).model_dump_json()))
    ```
  </Step>
</Steps>

### Creating the main handler

Finally, implement the main function to process multiple files concurrently:

```python theme={null}
async def main():
    data = [
        ("example_data/ExampleFileTextFormat.txt", UserExtract, None),
        ("example_data/ExampleFilePDFFormat.pdf", UserExtract, "out/"),
    ]
    tasks = []

    for row in data:
        tasks.append(handle_file(row[0], row[1], row[2]))

    await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())
```

In this example, the input paths are hardcoded, but you could modify the application to accept input paths from the command line or a web interface, or read from a directory or database.

### Testing the application

Your data repair tool is now ready to use. To test it, follow these steps:

<Steps>
  <Step title="Create an `example_data` directory">
    Create an `example_data` directory and add some test files:

    * A text file with user information
    * A PDF file with user information

    You can use the [example data](https://github.com/writer/api-tutorials/tree/main/instructor-and-writer-tutorial/example_data) provided in the GitHub repository for this tutorial. If you provide your own, be sure to update the `main.py` file to point to the new files.
  </Step>

  <Step title="Run the application">
    Run the application:

    ```bash theme={null}
    poetry run python main.py
    ```

    The application will process both files concurrently and generate CSV files containing the extracted user information.
  </Step>
</Steps>

## Conclusion

You've now seen basic and advanced usage of Writer with Instructor. To learn more about Instructor, check out the [Instructor documentation](https://python.useinstructor.com/). Structured output is a powerful feature that can help you build more accurate and reliable applications, especially combined with [tool calling](/home/tool-calling).


# Using Writer with LangChain
Source: https://dev.writer.com/home/integrations/langchain



The [Writer LangChain integration](https://github.com/writer/langchain-writer) allows you to leverage Writer's capabilities within the LangChain ecosystem, making it easy to build sophisticated AI applications. In this tutorial, you'll explore each component of the integration and understand how they work.

## Prerequisites

Before you begin, make sure you have:

* Python 3.9 or higher installed
* A [Writer AI Studio](https://app.writer.com/register) account
* A Writer API key. See instructions in the [API Quickstart](/home/quickstart).
* Basic familiarity with Python and [LangChain concepts](https://python.langchain.com/docs/concepts/)

## Installation and setup

First, install the necessary packages:

```bash theme={null}
pip install langchain-writer python-dotenv
```

Next, create a `.env` file with `WRITER_API_KEY` set to your Writer API key:

```
WRITER_API_KEY=<your-api-key>
```

## Components of the Writer LangChain integration

The `langchain-writer` package provides several key components:

1. `ChatWriter` for text generation
2. Tool calling capabilities, including:
   * `GraphTool` for Knowledge Graph integration
   * `NoCodeAppTool` for no-code applications
   * `LLMTool` for model delegation
3. Additional tools like `PDFParser` for parsing PDFs

## ChatWriter

`ChatWriter` is a LangChain chat model that provides access to Writer's AI capabilities for text generation. It supports streaming, non-streaming, batching, and asynchronous operations. You can use any of the [Palmyra chat models](/home/models) available in AI Studio.

See the [full documentation](https://github.com/writer/langchain-writer/blob/main/docs/chat_writer.md#parameters) for `ChatWriter` to learn more about the available parameters.

### Usage

This example uses `ChatWriter` to ask the Palmyra X5 model to explain what LangChain is in plain terms.

```python theme={null}
from langchain_writer import ChatWriter
from dotenv import load_dotenv

load_dotenv()

# Initialize the chat model
# These are optional parameters with default values listed here
chat = ChatWriter(
    model="palmyra-x5",  # default model
    temperature=0.7,        # controls randomness (0-1)
    max_tokens=None,        # maximum number of tokens to generate
    timeout=None,           # request timeout
    max_retries=2          # number of retries on failure
)

# Generate a response
response = chat.invoke("Explain what LangChain is in simple terms.")
print(response.content)
```

### Streaming

Streaming allows you to receive the generated text in chunks as it's being produced. This example shows how to stream a response from the Palmyra X5 model using synchronous streaming:

```python theme={null}
from langchain_writer import ChatWriter
import asyncio
from dotenv import load_dotenv

load_dotenv()

chat = ChatWriter()

# Streaming (synchronous)
for chunk in chat.stream("Write a short poem about artificial intelligence."):
    print(chunk.content, end="")
```

You can also use asynchronous streaming with the `async for` loop and the `astream` method:

```python theme={null}
from langchain_writer import ChatWriter
import asyncio
from dotenv import load_dotenv

load_dotenv()

chat = ChatWriter()

# Streaming (asynchronous)
async def async_stream():
    async for chunk in chat.astream("Write a short poem about artificial intelligence."):
        print(chunk.content, end="")

asyncio.run(async_stream())
```

### Batch processing

You can batch process multiple prompts for efficient processing. The following example batches three individual LLM invocations and runs them in parallel:

```python theme={null}
from langchain_writer import ChatWriter
from dotenv import load_dotenv

load_dotenv()

chat = ChatWriter()

questions = [
    "What is a three-sentence definition of retrieval-augmented generation?",
    "When did transformer architecture first appear in the literature?",
    "What are 3 benefits of using AI to assist in software development?"
]

# Process multiple prompts in parallel
responses = chat.batch(questions)

for question, response in zip(questions, responses):
    print(f"Q: {question}")
    print(f"A: {response.content}\n")
```

Note that `batch` returns results in the same order as the inputs. You can use `batch_as_completed` to return results as they complete. Results may arrive out of order, but each includes the input index for matching.

You can also optionally set the `max_concurrency` parameter to control the number of concurrent requests, which can be useful when you want to limit the number of parallel calls to prevent overloading a server or API:

```python theme={null}
responses = chat.batch(questions,
    config={"max_concurrency": 2}
)
```

See the LangChain documentation on [parallel execution](https://python.langchain.com/docs/concepts/runnables/#optimized-parallel-execution-batch) for more information.

## Tool calling

`ChatWriter` supports tool calling, which allows the model to use external functions to enhance its capabilities. Tool calling is available with Palmyra X4 and later.

### Tool calling basics

To use tool calling, follow these steps:

1. Define a function that will be called by the model and decorate it with the `@tool` decorator.
2. Bind the tool to the chat model using the `bind_tools` method.
3. Use the tool in a chat and append the response to the messages list.
4. Execute the tool call with the arguments given by the model and append the response to the messages list.
5. Invoke the chat model with the updated messages list to receive the final response.

Here's an example of how to use tool calling:

```python theme={null}
from langchain_writer import ChatWriter
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from dotenv import load_dotenv

load_dotenv()

@tool
def get_weather(location: str) -> str:
    """Get the current weather for a location."""
    # In a real application, you would call a weather API here
    return f"The weather in {location} is sunny and 75°F"

chat = ChatWriter()
chat_with_tools = chat.bind_tools([get_weather])

messages = [
    HumanMessage(
        "What's the weather like in San Francisco?"
    )
]

response = chat_with_tools.invoke(messages)
messages.append(response)

for tool_call in response.tool_calls:
    selected_tool = {
        "get_weather": get_weather,
    }[tool_call["name"].lower()]
    tool_msg = selected_tool.invoke(tool_call)
    messages.append(tool_msg)

response = chat_with_tools.invoke(messages)
print(response.content)
```

### GraphTool

`GraphTool` is a LangChain tool that allows you to retrieve information from a Knowledge Graph to enhance its responses. The tool executes remotely, so you simply need to provide the IDs of the Knowledge Graph you want to use. For more details on the built-in Knowledge Graph chat tool in Writer, see the [Knowledge Graph chat support guide](/home/kg-chat).

#### Usage

```python theme={null}
from langchain_writer import ChatWriter
from langchain_writer.tools import GraphTool
from dotenv import load_dotenv

load_dotenv()

# Initialize the chat model
chat = ChatWriter()

# Create a graph tool with your knowledge graph ID
graph_tool = GraphTool(graph_ids=["your-knowledge-graph-id"])

# Bind the tool to the chat model
chat_with_tools = chat.bind_tools([graph_tool])

# Ask a question that can be answered using the knowledge graph
response = chat_with_tools.invoke("What information do you have about product X?")
print(response.content)
```

### NoCodeAppTool

`NoCodeAppTool` is a specialized tool that enables access to Writer's no-code applications as LLM tools. This tool allows language models to interact with pre-built applications to enhance their responses.

The tool is designed as a standard "function" type tool and requires manual execution. Unlike the `GraphTool` and `LLMTool` which are executed remotely by Writer's servers, you'll need to handle the execution of the `NoCodeAppTool` in your code.

When initializing the tool, you must provide an `app_id` (either directly or using an environment variable) that corresponds to a no-code application created in your Writer account. The tool automatically retrieves the input parameters for the app during initialization, so you don't need to specify them manually.

You can customize the tool's name (default is "No-code application") and description (default is "No-code application powered by Palmyra") to provide better context to the model. The API key is typically read from environment variables but can also be provided directly.

When working with the tool, remember that all required inputs must be provided when invoking it, or a `ValueError` will be raised. Input values can be either strings or lists of strings, depending on what the no-code application expects.

This tool is particularly useful for generating content, performing specialized data transformations, creating custom outputs based on user inputs, integrating with domain-specific applications, enhancing responses with formatted content, or leveraging pre-built applications for common tasks.

#### Usage

Here's an example of how to use the `NoCodeAppTool`:

```python theme={null}
from langchain_writer import ChatWriter
from langchain_writer.tools import NoCodeAppTool
from langchain_core.messages import HumanMessage
import os
from dotenv import load_dotenv

load_dotenv()

chat = ChatWriter()

# Create a NoCodeAppTool
app_tool = NoCodeAppTool(
    app_id=os.getenv("APP_ID"), 
    name="Social post generator", 
    description="No-code app that generates social posts from product descriptions"
)

# Bind the tool ChatWriter
chat_with_tools = chat.bind_tools([app_tool])

product_description = "The Terra running shoe is a high-performance, lightweight shoe that offers a comfortable and durable experience. The shoe is also lightweight and easy to carry, making it an ideal option for runners looking for a new pair of shoes."

# Create a conversation
messages = [
    HumanMessage("Can you help me generate a social post about a new running shoe? Here is the product description:\n\n" + product_description)
]

# Get the model's response
response = chat_with_tools.invoke(messages)
messages.append(response)

# If the model requests to use the tool
if response.tool_calls:
    for tool_call in response.tool_calls:
        print("\nTool call:", tool_call)
        
        # Dynamically build the inputs dictionary from the args
        inputs = {}
        for arg in tool_call['args']:
            inputs[arg] = tool_call['args'][arg]
        
        # Execute the tool call
        try:
            tool_response = app_tool.run(tool_input={"inputs": inputs})
            messages.append(tool_response.suggestion)
        except Exception as e:
            print(f"Error running tool: {e}")
    
    # Get the final response
    try:
        final_response = chat_with_tools.invoke(messages)
        print("\nFinal response:", final_response.content)
    except Exception as e:
        print(f"Error getting final response: {e}")
else:
    print("No tool calls were requested.")
```

When providing inputs to the tool, you'll need to format them as a dictionary:

```python theme={null}
# Example of providing inputs to the tool
inputs = {
    "Input name": "Input value",
    "Another input name": ["List", "of", "string", "values"]
}

# Invoke the tool with the inputs
result = app_tool.run(tool_input={"inputs": inputs})
```

### LLMTool

`LLMTool` is a specialized tool that enables delegation to another model. This tool allows a primary model to delegate calls to a different model—including [external models](/home/external-models) configured for your organization—to enhance its responses.

Unlike most LangChain tools that use the standard "function" type, the `LLMTool` has a type of `llm` and is designed specifically for use within the Writer environment. Due to its remote execution nature, this tool doesn't support direct invocation through the `_run` method—attempting to call this method will raise a `NotImplementedError`.

When initializing the tool, you specify which model to delegate to using the `model_name` parameter. This can be a Palmyra model like `palmyra-x5` or an [external model](/home/external-models) configured for your organization. The default model is `palmyra-x5`. The `description` parameter plays a crucial role in helping the model understand when to use this tool.

When the model uses the LLM tool, the execution happens remotely on Writer's servers, and the response includes additional data in the `additional_kwargs["llm_data"]` field that you can access in your application.

This tool is valuable when you want to delegate a focused task to another model.

#### Usage

Here's an example of how to use the `LLMTool`:

```python theme={null}
from langchain_writer import ChatWriter
from langchain_writer.tools import LLMTool
from dotenv import load_dotenv

load_dotenv()

chat = ChatWriter()

# Create an LLMTool that delegates coding tasks to a model specialized in code generation
llm_tool = LLMTool(
    model_name="anthropic.claude-3-sonnet-20240229-v1:0",
    description="A function that invokes a model specialized in writing and debugging code. Use this tool for any request that involves generating, explaining, or fixing code."
)

# Bind the tool to the ChatWriter
chat_with_tools = chat.bind_tools([llm_tool])

# The primary model delegates the coding request to the specialized model
response = chat_with_tools.invoke([
    ("system", "You are a helpful assistant. Delegate coding tasks to the code generation tool."),
    ("human", "Write a Python function that returns the nth Fibonacci number using memoization.")
])

print(response.content)

# The LLM data is available in the response
print(response.additional_kwargs["llm_data"])
```

## Additional tools

### PDFParser

`PDFParser` is a document loader that uses Writer's PDF parsing capabilities to extract text from PDF documents. It converts PDFs into LangChain Document objects that can be used in your applications. For more details on the underlying API, see the [PDF parser API reference](/api-reference/tool-api/pdf-parser).

#### Usage

Here's an example of how to use the `PDFParser`:

```python theme={null}
from langchain_writer import PDFParser
from langchain_core.documents.base import Blob
from dotenv import load_dotenv

load_dotenv()

# Initialize the PDF parser with the desired output format
parser = PDFParser(output_format="markdown")  # Options: "text", "markdown"

# Load a PDF file
file = Blob.from_path("path/to/your/document.pdf")

parsed_pages = parser.parse(blob=file)
print(parsed_pages)
```

#### Output formats

The `PDFParser` supports different output formats:

* `text`: Plain text extraction
* `markdown`: Structured markdown with preserved formatting

```python theme={null}
# For markdown output
markdown_parser = PDFParser(output_format="markdown")
markdown_docs = markdown_parser.load("path/to/your/document.pdf")
```

## Conclusion

In this tutorial, you've explored the LangChain integration with Writer, covering each of its components:

1. `ChatWriter` for text generation
2. Tool calling capabilities, including:
   * `GraphTool` for Knowledge Graph integration
   * `NoCodeAppTool` for no-code applications
   * `LLMTool` for model delegation
3. Additional tools like `PDFParser` for parsing PDFs

This integration provides a foundation for building AI applications with Writer and LangChain. Check out the package [README](https://github.com/writer/langchain-writer) and [documentation](https://github.com/writer/langchain-writer/tree/main/docs), as well as the [LangChain Documentation](https://python.langchain.com/docs/) for more information on how to use LangChain with Writer.


# Using Writer with OpenLLMetry
Source: https://dev.writer.com/home/integrations/openllmetry

Monitor and debug Writer API calls with OpenLLMetry. Export traces to Jaeger, Datadog, New Relic, or Traceloop for LLM observability.

This guide shows you how to integrate Writer with [OpenLLMetry](https://www.traceloop.com/openllmetry) for monitoring and debugging your LLM applications. After completing these steps, you can trace and observe your Writer API calls alongside other LLM operations in your observability stack.

<img alt="Traceloop dashboard showing Writer integration with OpenLLMetry" />

## What is OpenLLMetry?

OpenLLMetry is an open source project that enables monitoring and debugging of LLM application execution. It provides non-intrusive tracing built on top of OpenTelemetry, allowing you to export traces to your existing observability stack.

Since it's built on [OpenTelemetry](https://opentelemetry.io/), you can use any OpenTelemetry compatible backend (Jaeger, Zipkin, Datadog, New Relic, etc.) or the hosted Traceloop platform.

## What you get with the Writer integration

The Writer integration with OpenLLMetry provides enhanced observability beyond standard OpenTelemetry traces. You'll see detailed information about your Writer API calls as additional span attributes:

* **`gen_ai.request.model`**: the model requested (for example, `palmyra-x5`)
* **`gen_ai.prompt`**: array of prompts sent to the Writer model
* **`gen_ai.completion`**: array of completions returned from Writer
* **`gen_ai.usage.total_tokens`**: total tokens used
* **`gen_ai.usage.prompt_tokens`**: number of tokens used for prompts
* **`gen_ai.usage.completion_tokens`**: number of tokens used for completions

This gives you comprehensive visibility into your Writer API usage, including prompt engineering insights, token consumption patterns, and performance characteristics that aren't available in standard OpenTelemetry traces.

<Note>
  For a complete list of supported attributes, see the full [GenAI Semantic Conventions](https://www.traceloop.com/docs/openllmetry/contributing/semantic-conventions).
</Note>

## Prerequisites

Before you begin, make sure you have:

* Python 3.11 or higher installed
* A [Writer AI Studio](https://app.writer.com/register) account
* A [Traceloop](https://auth.traceloop.com/en/login) account
* A Writer API key. See instructions in the [API Quickstart](/home/quickstart)
* Basic familiarity with Python and [OpenLLMetry concepts](https://www.traceloop.com/docs/openllmetry/introduction)

<Note>
  The example below uses **Traceloop** as the observability provider. See the [connect to an external provider](#connect-to-external-providers) section to learn how to connect to other providers.
</Note>

## Configuration details

| Environment Variable      | Required        | Default                     | Description                                                                                                                                                                                                                                                                                                                                 |
| ------------------------- | --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WRITER_API_KEY`          | **Required**    | None                        | Writer API key for authenticating API calls. Must be set for the application to function.                                                                                                                                                                                                                                                   |
| `TRACELOOP_API_KEY`       | **Conditional** | None                        | **Required for Traceloop Cloud**: Create an API key in your [Traceloop dashboard](https://app.traceloop.com/settings/api-keys) and set this environment variable. **Optional for external providers**: When connecting to other observability platforms (Datadog, New Relic, etc.), omit this variable and use `TRACELOOP_HEADERS` instead. |
| `TRACELOOP_BASE_URL`      | Optional        | `https://api.traceloop.com` | OpenTelemetry endpoint to connect to. If prefixed with `http`/`https`, uses [OTLP/HTTP protocol; otherwise uses OTLP/GRPC](https://opentelemetry.io/docs/specs/otel/protocol/exporter/). The SDK appends `/v1/traces`.                                                                                                                      |
| `TRACELOOP_HEADERS`       | Optional        | None                        | [Custom HTTP headers for authentication](https://www.traceloop.com/docs/openllmetry/configuration#headers). If set, API key is ignored.                                                                                                                                                                                                     |
| `TRACELOOP_TRACE_CONTENT` | Optional        | `true`                      | [Enable/disable logging of prompts, completions, and embeddings to span attributes](https://www.traceloop.com/docs/openllmetry/privacy/traces#disabling-logging-globally).                                                                                                                                                                  |
| `TRACELOOP_TELEMETRY`     | Optional        | `true`                      | [Enable/disable anonymous telemetry data collection](https://www.traceloop.com/docs/openllmetry/configuration#control-telemetry).                                                                                                                                                                                                           |

<Note>
  For additional configuration option details, see the [SDK initialization docs](https://www.traceloop.com/docs/openllmetry/configuration).
</Note>

## Integrate Writer with OpenLLMetry

### 1. Install the SDK

Install OpenLLMetry in your Python environment:

```bash theme={null}
pip install traceloop-sdk
```

### 2. Initialize and configure

In your Writer application, initialize the Traceloop tracer. For local development, you may want to disable batch sending to see traces immediately.

<CodeGroup>
  ```python theme={null}
  import os
  from traceloop.sdk import Traceloop
  from writerai import Writer

  # Initialize Traceloop for Writer monitoring
  Traceloop.init(
      app_name="YOUR_APP_NAME",
      api_key=os.environ.get("TRACELOOP_API_KEY"),
      # disable_batch=True   # For local development, this disables batch sending so you can see Writer API traces immediately
  )

  # Initialize Writer client
  client = Writer(api_key=os.environ.get("WRITER_API_KEY"))
  ```
</CodeGroup>

### 3. (Optional) annotate your workflows

[Workflow annotations](https://www.traceloop.com/docs/openllmetry/tracing/annotations) help organize and group related operations for better trace visualization and debugging.

**When to use decorators:**

* **Complex processes**: Group related API calls and business logic into logical workflows
* **Custom naming**: Override automatic span naming with business-context names
* **Hierarchical tracing**: Create parent-child relationships between workflows and tasks

**Available decorators:**

| Decorator   | Purpose                                | Use case                                                        |
| ----------- | -------------------------------------- | --------------------------------------------------------------- |
| `@workflow` | Multi-step processes or "chains"       | Complete business workflows that can be traced as a single unit |
| `@task`     | Individual operations within workflows | Specific actions or computations within a workflow              |
| `@agent`    | Autonomous agents                      | AI agents that can make decisions and use tools                 |
| `@tool`     | Agent tools                            | Functions that agents can call to perform specific actions      |

<Note>
  Workflow annotations are **optional**. OpenLLMetry traces Writer API calls automatically without needing any decorators or additional code.
</Note>

```python theme={null}
from traceloop.sdk.decorators import workflow, task, agent, tool

# Workflow and task example
@workflow(name="content_creation")
def content_creation_pipeline(topic: str):
    research = research_topic(topic)
    content = generate_content(research)
    return content

@task(name="research_topic")
def research_topic(topic: str):
    completion = client.chat.completions.create(
        model="palmyra-x5",
        messages=[{"role": "user", "content": f"Provide a comprehensive overview of {topic}, including key concepts, benefits, and current trends."}],
        max_tokens=300
    )
    return completion.choices[0].message.content

@task(name="generate_content")
def generate_content(research: str):
    completion = client.chat.completions.create(
        model="palmyra-x5",
        messages=[{"role": "user", "content": f"Write an engaging article based on this research: {research}"}],
        max_tokens=500
    )
    return completion.choices[0].message.content

# Agent and tool example
@agent(name="content_agent")
def content_agent(request: str):
    return research_tool(request)

@tool(name="research_tool")
def research_tool(query: str):
    completion = client.chat.completions.create(
        model="palmyra-x5",
        messages=[{"role": "user", "content": f"Find detailed information about {query} and provide key insights and data points."}],
        max_tokens=300
    )
    return completion.choices[0].message.content
```

## Connect to external providers

Because the Traceloop SDK is built on OpenTelemetry, the data it generates can be used in any observability platform that supports the OpenTelemetry standard. OpenLLMetry extends this by using the OTLP protocol to connect with external observability providers, letting you send traces directly to your existing stack without relying on Traceloop’s hosted platform.

### Configure external provider connection

To connect to an external provider, configure the following environment variables:

| Environment Variable | Description                                           |
| -------------------- | ----------------------------------------------------- |
| `TRACELOOP_BASE_URL` | The OTLP endpoint URL for your observability provider |
| `TRACELOOP_HEADERS`  | Authentication headers (if required by your provider) |

### Example configurations

#### [Grafana Cloud](https://www.traceloop.com/docs/openllmetry/integrations/grafana#without-grafana-agent)

```bash theme={null}
# Generate base64 encoded credentials
echo -n "<your stack id>:<your api key>" | base64

# Set environment variables
export TRACELOOP_BASE_URL=https://otlp-gateway-<zone>.grafana.net/otlp
export TRACELOOP_HEADERS="Authorization=Basic%20<base64 encoded stack id and api key>"
```

#### [Datadog](https://www.traceloop.com/docs/openllmetry/integrations/datadog)

```bash theme={null}
# Connect to your Datadog Agent (requires OTLP HTTP collector enabled)
export TRACELOOP_BASE_URL="http://<datadog-agent-hostname>:4318"
```

#### [New Relic](https://www.traceloop.com/docs/openllmetry/integrations/newrelic)

```bash theme={null}
export TRACELOOP_BASE_URL=https://otlp.nr-data.net:443
export TRACELOOP_HEADERS="api-key=<YOUR_NEWRELIC_LICENSE_KEY>"
```

<Note>
  For a complete list of supported integrations, see the full [OpenLLMetry integrations catalog](https://www.traceloop.com/docs/openllmetry/integrations/introduction#the-integrations-catalog).
</Note>

## Next steps

Now that you've set up Writer with OpenLLMetry, you can start monitoring your LLM applications. See the following resources to help you get the most out of your integration:

* Explore [OpenLLMetry's advanced features](https://docs.traceloop.com/docs/openllmetry/tracing) for more detailed tracing capabilities
* Learn about [workflow annotations](https://docs.traceloop.com/docs/openllmetry/tracing/workflow-annotations) to better organize your traces
* Set up [custom integrations](https://docs.traceloop.com/docs/openllmetry/integrations) with your existing observability stack
* Review the [Writer OpenLLMetry integration](https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-writer) for additional details and examples


# Using Writer with AWS Strands Agents
Source: https://dev.writer.com/home/integrations/strands



Writer's models are available on [AWS Strands Agents](https://strandsagents.com/latest/).

Strands Agents SDK from AWS is an open-source framework that enables developers to build and deploy AI agents using a model-driven approach. This integration allows you to use Writer models within the Strands agent ecosystem, from local development to production deployment.

## Available models

Writer offers several specialized Palmyra models:

| Model      | Model ID (Writer API) | Model ID (Bedrock)          | Context Window | Availability                                                                               | Description                                                                                        |
| ---------- | --------------------- | --------------------------- | -------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| Palmyra X5 | `palmyra-x5`          | `us.writer.palmyra-x5-v1:0` | 1M tokens      | [Writer API](#writer-api-configuration) or [Amazon Bedrock](#amazon-bedrock-configuration) | Latest model with 1 million token context for complex workflows, supports vision and multi-content |
| Palmyra X4 | `palmyra-x4`          | `us.writer.palmyra-x4-v1:0` | 128k tokens    | [Writer API](#writer-api-configuration) or [Amazon Bedrock](#amazon-bedrock-configuration) | Advanced model for workflow automation and tool calling                                            |

See the [Writer models guide](/home/models) for more details and use cases.

<Note>
  The above Bedrock models IDs for Palmyra X5 and Palmyra X4 are using [cross-region inference profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). Check which AWS Regions support Writer models before selecting an inference profile.
</Note>

## Prerequisites

Before you begin, make sure you have:

* Python 3.10 or higher installed
* Basic familiarity with Python and [AWS Strands](https://strandsagents.com/latest/)

## Writer API configuration

### Prerequisites

Before you configure the WRITER API client for Strands Agent SDK, make sure you have:

* A [Writer AI Studio](https://app.writer.com/register) account
* A Writer API key. See instructions in the [API Quickstart](/home/quickstart)

### Installation

To use Writer models with Strands Agents, install the optional Writer dependency:

```bash theme={null}
pip install 'strands-agents[writer]'
```

<Note>
  To follow along with the examples in this guide, you'll also need the [Strands Agent Tools package](https://github.com/strands-agents/tools). Install the package with `pip install strands-agents-tools`.
</Note>

### Client configuration

You can pass additional arguments to the Writer client via `client_args`:

```python theme={null}
model = WriterModel(
    client_args={
        "api_key": "<WRITER_API_KEY>",
        "timeout": 30,
        "base_url": "https://api.writer.com/v1",
        # Additional client configuration options
    },
    model_id="palmyra-x5"
)
```

### Environment variables

You can set your Writer API key as an environment variable instead of passing it directly:

```bash theme={null}
export WRITER_API_KEY="your_api_key_here"
```

Then initialize the model without the `client_args["api_key"]` parameter:

```python theme={null}
model = WriterModel(model_id="palmyra-x5")
```

### Usage

After installing, you can import and initialize the Writer provider in Strands Agents:

```python theme={null}
from strands import Agent
from strands.models.writer import WriterModel
from strands_tools import calculator

model = WriterModel(
    client_args={"api_key": "<WRITER_API_KEY>"},
    model_id="palmyra-x5",
)

agent = Agent(model=model, tools=[calculator])
response = agent("What is 2+2")
print(response)
```

<Note>
  By default, Strands Agents use a `PrintingCallbackHandler` that streams responses to `stdout` as they're generated. When you call `agent("What is 2+2")`, you'll see the response appear in real-time as it's being generated. The `print(response)` above also shows the final collected result after the response is complete. See [Callback Handlers](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/streaming/callback-handlers/?h=callback) in the Strands documentation for more details.
</Note>

## Amazon Bedrock configuration

You can also use Writer models through Amazon Bedrock, which provides a managed service for accessing foundation models.

### Prerequisites

Before you configure the Amazon Bedrock client for Strands Agent SDK, follow the [Bedrock integration guide](/home/integrations/bedrock) to set up AWS credentials, enable Writer models in your region, and set up the required IAM permissions.

### Installation

Install the base Strands Agents package:

```bash theme={null}
pip install strands-agents
```

Bedrock is the default model provider in Strands Agents, so no additional model provider installation is required. The `BedrockModel` class is available by default.

<Note>
  To follow along with the examples in this guide, you'll also need the [Strands Agent Tools package](https://github.com/strands-agents/tools). Install the package with `pip install strands-agent-tools`.
  To follow along with the examples in this guide, you'll also need the [Strands Agent Tools package](https://github.com/strands-agents/tools). Install the package with `pip install strands-agents-tools`.
</Note>

### Client configuration

The following client configuration uses the `us.writer.palmyra-x5-v1:0` model ID, which is the ID for the [cross-region inference profile for Palmyra X5](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). Check which AWS Regions support Writer models before selecting an inference profile.

```python theme={null}
# Configure Bedrock model with Writer model ID
model = BedrockModel(
    model_id="us.writer.palmyra-x5-v1:0",
    region_name="us-west-2",  # Your preferred AWS region
    # Additional client configuration options
)
```

### Usage

After configuring the Bedrock model, you can use it with Strands Agents:

```python theme={null}
from strands import Agent
from strands.models.bedrock import BedrockModel
from strands_tools import calculator

# Configure Bedrock model with Writer model ID
model = BedrockModel(
    model_id="us.writer.palmyra-x5-v1:0",
    region_name="us-west-2"
)

agent = Agent(model=model, tools=[calculator])
response = agent("What is 2+2")
print(response)
```

## Model configuration

The `WriterModel` accepts configuration parameters as keyword arguments to the model constructor:

| Parameter        | Type                              | Description                                                                                                                                                                                             | Default                                                              | Options                                                                     |
| ---------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `model_id`       | `str`                             | Model name to use (`palmyra-x5`, `palmyra-x4`, etc.)                                                                                                                                                    | Required                                                             | [reference](/home/models)                                                   |
| `max_tokens`     | `Optional[int]`                   | Maximum number of tokens to generate                                                                                                                                                                    | See the Context Window for [each available model](#available-models) | [reference](/api-reference/completion-api/chat-completion#body-max-tokens)  |
| `stop`           | `Optional[Union[str, List[str]]]` | A token or sequence of tokens that, when generated, causes the model to stop producing further content. This can be a single token or an array of tokens, acting as a signal to end the output.         | `None`                                                               | [reference](/api-reference/completion-api/chat-completion#body-stop)        |
| `stream_options` | `Dict[str, Any]`                  | Additional options for streaming. Specify `include_usage` to include usage information in the response, in the `accumulated_usage` field. If you don't specify this, `accumulated_usage`for each value. | `None`                                                               | [reference](/api-reference/completion-api/chat-completion#body-stream)      |
| `temperature`    | `Optional[float]`                 | What sampling temperature to use (0.0 to 2.0). A higher temperature produces more random output.                                                                                                        | `1`                                                                  | [reference](/api-reference/completion-api/chat-completion#body-temperature) |
| `top_p`          | `Optional[float]`                 | Threshold for "nucleus sampling"                                                                                                                                                                        | `None`                                                               | [reference](/api-reference/completion-api/chat-completion#body-top_p)       |

## Examples

### Writer API integration

#### Enterprise workflow automation

```python theme={null}
from strands import Agent
from strands.models.writer import WriterModel
from my_tools import web_search, email_sender  # Custom tools from your local module

model = WriterModel(
    client_args={"api_key": "<WRITER_API_KEY>"},
    model_id="palmyra-x5",
)

agent = Agent(
    model=model,
    tools=[web_search, email_sender],
    system_prompt="You are an enterprise assistant that helps automate business workflows."
)

response = agent("Research our competitor's latest product launch and draft a summary email for the leadership team")
```

<Note>
  The `web_search` and `email_sender` tools in this example are custom tools that you would need to define. See [Python Tools](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/tools/python-tools/?h=python+tools) for guidance on creating custom tools, or use existing tools from the [`strands_tools` package](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/tools/example-tools-package/).
</Note>

#### Financial analysis with Palmyra X5

```python theme={null}
from strands import Agent
from strands.models.writer import WriterModel

# Use Palmyra X5 for financial analysis
model = WriterModel(
    client_args={"api_key": "<WRITER_API_KEY>"},
    model_id="palmyra-x5"
)

agent = Agent(
    model=model,
    system_prompt="You are a financial analyst assistant. Provide accurate, data-driven analysis."
)

# Replace the placeholder with your actual financial report content
actual_report = """
[Your quarterly earnings report content would go here - this could include:
- Revenue figures
- Profit margins
- Growth metrics
- Risk factors
- Market analysis
- Any other financial data you want analyzed]
"""

response = agent(f"Analyze the key financial risks in this quarterly earnings report: {actual_report}")
```

#### Long-context document processing

```python theme={null}
from strands import Agent
from strands.models.writer import WriterModel

# Use Palmyra X5 for processing very long documents
model = WriterModel(
    client_args={"api_key": "<WRITER_API_KEY>"},
    model_id="palmyra-x5",
    temperature=0.2
)

agent = Agent(
    model=model,
    system_prompt="You are a document analysis assistant that can process and summarize lengthy documents."
)

# Can handle documents up to 1M tokens
# Replace the placeholder with your actual document content
actual_transcripts = """
[Meeting transcript content would go here - this could be thousands of lines of text
from meeting recordings, documents, or other long-form content that you want to analyze]
"""

response = agent(f"Summarize the key decisions and action items from these meeting transcripts: {actual_transcripts}")
```

#### Vision and image analysis

Palmyra X5 supports vision capabilities, allowing you to analyze images and extract information from visual content.

```python theme={null}
from strands import Agent
from strands.models.writer import WriterModel

# Use Palmyra X5 for vision tasks
model = WriterModel(
    client_args={"api_key": "<WRITER_API_KEY>"},
    model_id="palmyra-x5"
)

agent = Agent(
    model=model,
    system_prompt="You are a visual analysis assistant. Provide detailed, accurate descriptions of images and extract relevant information."
)

# Read the image file
with open("path/to/image.png", "rb") as image_file:
    image_data = image_file.read()

messages = [
    {
        "role": "user",
        "content": [
            {
                "image": {
                    "format": "png",
                    "source": {
                        "bytes": image_data
                    }
                }
            },
            {
                "text": "Analyze this image and describe what you see. What are the key elements, colors, and any text or objects visible?"
            }
        ]
    }
]

# Create an agent with the image message
vision_agent = Agent(model=model, messages=messages)

# Analyze the image
response = vision_agent("What are the main features of this image and what might it be used for?")

print(response)
```

### Bedrock integration

#### Structured output generation

Palmyra X5 and X4 support structured output generation using [Pydantic models](https://docs.pydantic.dev/latest/) through Bedrock. This is useful for ensuring consistent, validated responses.

<Note>
  Structured output disables streaming and returns the complete response at once, unlike regular chat completions, which stream by default. See [Callback Handlers](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/streaming/callback-handlers/?h=callback) for more details.
</Note>

```python theme={null}
import os
from boto3 import session
from dotenv import load_dotenv
from pydantic import BaseModel
from typing import List
from strands import Agent
from strands.models import BedrockModel

load_dotenv()

# Define a structured schema for marketing campaign
class MarketingCampaign(BaseModel):
    campaign_name: str
    target_audience: str
    key_messages: List[str]
    channels: List[str]
    budget_allocation: str
    success_metrics: List[str]
    timeline: str
    call_to_action: str

# Use Writer Palmyra X5 through Bedrock
bedrock_model = BedrockModel(
    model_id='us.writer.palmyra-x5-v1:0',
    boto_session=session.Session(
        aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", ""),
        aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", ""),
        aws_session_token=os.getenv("AWS_SESSION_TOKEN", ""),
        region_name="us-west-2",
    ),
    streaming=False,
)

agent = Agent(
    model=bedrock_model,
    system_prompt="You are a marketing strategist. Create comprehensive marketing campaigns with structured data for enterprise clients."
)

# Generate structured marketing campaign
response = agent.structured_output(
    output_model=MarketingCampaign,
    prompt="Create a B2B marketing campaign for a cloud infrastructure platform targeting enterprise IT decision-makers and CTOs."
)

print(f"Campaign: {response.campaign_name}\nTarget Audience: {response.target_audience}\nKey Messages: {response.key_messages}\nChannels: {response.channels}\nBudget: {response.budget_allocation}\nMetrics: {response.success_metrics}\nTimeline: {response.timeline}\nCall to Action: {response.call_to_action}")
```

#### Memory agent

This example demonstrates how to use Writer models through Bedrock with memory capabilities. The `mem0_memory` tool from `strands_tools` provides persistent, long-term memory that stores information using [Mem0's memory architecture](https://strandsagents.com/latest/documentation/docs/examples/python/memory_agent/), allowing the agent to remember user preferences, facts, and context across multiple sessions. The memory system uses semantic search to retrieve relevant information and can store, list, and retrieve memories based on user queries.

```python theme={null}
import os
import logging

from boto3 import session
from dotenv import load_dotenv

from strands import Agent
from strands.models import BedrockModel
from strands_tools import mem0_memory, use_llm

logger = logging.getLogger(__name__)

# Load environment variables
load_dotenv()

USER_ID = "mem0_user"

# System prompt for the memory agent
MEMORY_SYSTEM_PROMPT = f"""You are a personal assistant that maintains context by remembering user details.

Capabilities:
- Store new information using mem0_memory tool (action="store")
- Retrieve relevant memories (action="retrieve")
- List all memories (action="list")
- Provide personalized responses

Key Rules:
- Always include user_id={USER_ID} in tool calls
- Be conversational and natural in responses
- Format output clearly
- Acknowledge stored information
- Only share relevant information
- Politely indicate when information is unavailable
"""

# Use Writer Palmyra X5 through Bedrock
bedrock_model = BedrockModel(
    model_id='us.writer.palmyra-x5-v1:0',
    boto_session=session.Session(
        aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", ""),
        aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", ""),
        aws_session_token=os.getenv("AWS_SESSION_TOKEN", ""),
        region_name="us-west-2",
    ),
    streaming=False,
)

# Create an agent with memory capabilities
memory_agent = Agent(
    model=bedrock_model,
    system_prompt=MEMORY_SYSTEM_PROMPT,
    tools=[mem0_memory, use_llm],
)

def initialize_demo_memories():
    """Initialize some demo memories to showcase functionality."""
    content = """My name is Alex. I like to travel and stay in Airbnbs rather than hotels. I am planning a trip to Japan next spring. I enjoy hiking and outdoor photography as hobbies. I have a dog named Max. My favorite cuisine is Italian food."""
    memory_agent.tool.mem0_memory(action="store", content=content, user_id=USER_ID)

# Example usage
if __name__ == "__main__":
    print("\n🧠 Memory Agent 🧠\n")
    
    # Initialize demo memories
    initialize_demo_memories()
    print("Demo memories initialized!")
    
    # Example interactions
    print("\nExample: What do you know about me?")
    response = memory_agent("What do you know about me?")
    print(f"Response: {response}")
    
    print("\nExample: Remember that I prefer window seats on flights")
    response = memory_agent("Remember that I prefer window seats on flights")
    print(f"Response: {response}")
    
    print("\nExample: What are my travel preferences?")
    response = memory_agent("What are my travel preferences?")
    print(f"Response: {response}")
```

## Additional resources

See more information and examples below:

* [Additional code examples for Strands Agents](https://github.com/writer/aws-examples/tree/main/strands-examples)
* [Strands Agents GitHub repository](https://github.com/strands-agents/sdk-python)
* [Writer models](/home/models)


# Writer AI Studio
Source: https://dev.writer.com/home/introduction

Build, deploy, and manage AI agents with enterprise controls

Writer AI Studio is a full-stack platform for building AI agents—from development to deployment to ongoing management. Choose your development approach, connect your data, and maintain visibility and control as your AI systems scale.

## Build AI agents

Writer offers multiple paths to build AI agents, each designed for different technical backgrounds and use cases.

<CardGroup>
  <Card
    title="No-code agents"
    icon={
  <img
    src="https://img.writer.com/app-images/No-code.svg"
    alt=""
    class="large-icon"
  />
}
  >
    Automate repetitive tasks, generate content, or answer questions about company data.

    <br />

    [Build with No-code](/no-code/introduction) →
  </Card>

  <Card
    title="Agent Builder"
    icon={
  <img
    src="https://img.writer.com/app-images/Writer-framework.svg"
    alt=""
    class="large-icon"
  />
}
  >
    Build feature-rich agents with a visual editor and optional coding.

    <br />

    [Build with Agent Builder](/agent-builder/quickstart) →
  </Card>

  <Card
    title="API and SDKs"
    icon={
  <img
    src="https://img.writer.com/app-images/Writer-API.svg"
    alt=""
    class="large-icon"
  />
}
  >
    Integrate AI capabilities into your own applications and services.

    <br />

    [Build with API and SDKs](/home/quickstart) →
  </Card>
</CardGroup>

→ Explore the [development options guide](/home/development_options) to compare approaches.

## Choose your models

Use Writer's Palmyra models or connect external models from providers like AWS Bedrock.

<CardGroup>
  <Card title="Palmyra models" icon="bolt" href="/home/models">
    Writer's family of models including Palmyra X5 for general tasks, plus specialized models for finance, healthcare, and creative content.
  </Card>

  <Card title="External models" icon="plug" href="/home/external-models">
    Connect models from AWS Bedrock to use Claude, Llama, Mistral, and other models alongside Palmyra.
  </Card>
</CardGroup>

## Manage your platform

Monitor usage, enforce guardrails, and maintain governance across your AI systems.

<CardGroup>
  <Card title="Guardrails" icon="shield-check" href="/home/guardrails">
    Enforce content safety, PII protection, and compliance policies across your AI agents.
  </Card>

  <Card title="Observability" icon="chart-line" href="/home/observability">
    Track organization-wide usage and costs, monitor individual agent performance, and review session logs.
  </Card>
</CardGroup>

## Integrate with your stack

Writer works with popular frameworks and observability tools.

<CardGroup>
  <Card title="Framework integrations" icon="puzzle-piece" href="/home/integrations-overview">
    Use Writer with LangChain, AWS Strands, Instructor, and more.
  </Card>

  <Card title="Observability exports" icon="arrow-right-from-bracket" href="/home/integrations/openllmetry">
    Export telemetry data to your existing observability tools via OpenLLMetry.
  </Card>
</CardGroup>

## Where to go for help

<CardGroup>
  <Card title="Help center" icon="headset" href="https://support.writer.com/">
    Need help with billing, account management, or enterprise support? Visit our help center.
  </Card>

  <Card title="Developer feedback" icon="envelope" href="mailto:dev-feedback@writer.com">
    Stuck on something? Have feedback on the docs? Send us an email.
  </Card>
</CardGroup>


# Use a Knowledge Graph in a chat
Source: https://dev.writer.com/home/kg-chat

Query Knowledge Graphs during chat completions with tool calling. Reference multiple graphs and configure query parameters for accurate responses.

This guide demonstrates how to send questions to a [Knowledge Graph](/home/knowledge-graph) during a [chat completion](/home/chat-completion). Knowledge Graph chat is a predefined tool you can use to reference a Knowledge Graph during a chat.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Tool structure

Knowledge Graph chat is a predefined tool supported by Palmyra X4 and later models, to be used with tool calling in the [chat endpoint](/api-reference/completion-api/chat-completion). To use Knowledge Graph chat, add the following object to the `tools` array when calling the chat endpoint:

| Parameter               | Type    | Description                                                                                                                                                                                                                     |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                  | string  | The type of tool. Must be `graph` for Knowledge Graph chat.                                                                                                                                                                     |
| `function`              | object  | An object containing the `graph_ids`, `description`, `subqueries`, and `query_config` parameters                                                                                                                                |
| `function.graph_ids`    | array   | An array of strings containing the graph IDs you wish to reference                                                                                                                                                              |
| `function.description`  | string  | A description of the graphs you are referencing. This helps the model understand when to use the Knowledge Graph tool in the chat. If there are multiple graphs, include a description for each, referencing the graph by name. |
| `function.subqueries`   | Boolean | A Boolean indicating whether to include the subqueries used by Palmyra in the response.                                                                                                                                         |
| `function.query_config` | object  | Configuration options for Knowledge Graph queries. See the [Query configuration parameters](#query-configuration-parameters) below.                                                                                             |

<Note>
  Find your Knowledge Graph ID using one of these methods:

  * Call the [Knowledge Graph list endpoint](/api-reference/kg-api/list-graphs) to retrieve all Knowledge Graphs with their IDs.
  * Locate the ID in the URL of the Knowledge Graph page in [AI Studio](https://app.writer.com/aistudio).

  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>

### Query configuration parameters

| Parameter            | Type    | Range              | Default | Description                                                                                                                                                                                                                                                                                                                                                                      |
| -------------------- | ------- | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `max_subquestions`   | integer | 1-10               | 6       | Maximum number of sub-questions to generate when processing complex queries.                                                                                                                                                                                                                                                                                                     |
| `search_weight`      | integer | 0-100              | 50      | Controls the balance between keyword and semantic search in ranking results.                                                                                                                                                                                                                                                                                                     |
| `grounding_level`    | number  | 0.0-1.0            | 0.0     | Controls how closely responses must match to source material.                                                                                                                                                                                                                                                                                                                    |
| `max_snippets`       | integer | 5-25 (recommended) | 30      | Maximum number of text snippets to retrieve from the Knowledge Graph for context. Works in concert with `search_weight` to control best matches vs broader coverage. **Note**: While technically supports 1-60, values below 5 may return no results due to RAG implementation. Recommended range is 5-25. Due to RAG system behavior, you may see more snippets than requested. |
| `max_tokens`         | integer | 100-8000           | 4000    | Maximum number of tokens the model can generate in the response.                                                                                                                                                                                                                                                                                                                 |
| `keyword_threshold`  | number  | 0.0-1.0            | 0.7     | Threshold for keyword-based matching when searching Knowledge Graph content.                                                                                                                                                                                                                                                                                                     |
| `semantic_threshold` | number  | 0.0-1.0            | 0.7     | Threshold for semantic similarity matching when searching Knowledge Graph content. Set higher for stricter relevance, lower for broader range.                                                                                                                                                                                                                                   |
| `inline_citations`   | Boolean | true/false         | false   | Whether to include inline citations within the response text.                                                                                                                                                                                                                                                                                                                    |

For detailed explanations and usage examples, see the [Knowledge Graph query configuration guide](/home/knowledge-graph-query-config).

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "model": "palmyra-x5",
          "messages": [
              {
                  "role": "user",
                  "content": "Which of our products contain both food coloring and chocolate?"
              }
          ],
          "tool_choice": "auto",
          "tools": [
              {
                  "type": "graph",
                  "function": {
                      "description": "Knowledge Graph containing information about Acme Inc. food products",
                      "graph_ids": [
                          "<GRAPH_ID>"
                      ],
                      "subqueries": true
                  }
              }
          ],
          "stream": true
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  tools = [{
      "type": "graph",
      "function": {
          "description": "Description of the graph(s)",
          "graph_ids": [
              "<GRAPH_ID>"
          ],
          "subqueries": True
      }
  }]

  messages = [{"role": "user", "content": "Which of our products contain both food coloring and chocolate?"}]

  response = client.chat.chat(
      model="palmyra-x5", 
      messages=messages, 
      tools=tools,  # The tools array defined earlier.
      tool_choice="auto",
      stream=True
  )

  for chunk in response:
      if chunk.choices[0].delta.content is not None:
          print(chunk.choices[0].delta.content, end="", flush=True)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const tools = [{
      type: "graph",
      function: {
          description: "Description of the graph(s)",
          graph_ids: [
              "<GRAPH_ID>"
          ],
          subqueries: true
      }
  }]

  let messages = [{role: "user", content: "Which of our products contain both food coloring and chocolate?"}]

  const response = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      tools: tools, // The tools array defined earlier.
      tool_choice: "auto",
      stream: true
  });

  for await (const chunk of response) {
      if (chunk.choices[0].delta.content) {
          process.stdout.write(chunk.choices[0].delta.content);
      }
  }
  ```
</CodeGroup>

<Note>
  You can only pass one prebuilt tool in the `tools` array at a time. However, you can pass multiple [custom tools](/home/tool-calling) in the same request.

  Prebuilt tools are:

  * Knowledge Graph tool
  * [LLM tool](/home/model-delegation)
  * [Translation tool](/home/translation-tool)
  * [Web search tool](/home/web-search-tool)
</Note>

### Response format

When a chat completion uses the Knowledge Graph tool, the response from the Knowledge Graph tool is in the `graph_data` object. That object contains the following fields:

| Parameter            | Type   | Description                                                                                                |
| -------------------- | ------ | ---------------------------------------------------------------------------------------------------------- |
| `sources`            | array  | An array of objects containing the source file IDs and snippets that helped the model answer the question. |
| `sources.file_id`    | string | The ID of the source file.                                                                                 |
| `sources.snippet`    | string | A snippet from the source file that helped the model answer the question.                                  |
| `status`             | string | The status of the query.                                                                                   |
| `subqueries`         | array  | An array of objects containing the subqueries used by Palmyra in the response.                             |
| `subqueries.query`   | string | The query used by Palmyra to answer the question.                                                          |
| `subqueries.answer`  | string | The answer to the question.                                                                                |
| `subqueries.sources` | array  | An array of objects containing the source file IDs and snippets that helped the model answer the question. |

The full response has the following structure.

The subqueries and sources shown are abbreviated for readability. If the `subqueries` parameter is `false`, or if the model doesn't need subqueries to answer the question, this array is be empty.

<CodeGroup>
  ```json streaming response [expandable] {12} theme={null}
  {
      "id": "1234",
      "object": "chat.completion.chunk",
      "choices": [
          {
              "index": 0,
              "finish_reason": "stop",
              "delta": {
                  "content": "None of our products contain both chocolate and food coloring. The products containing chocolate are different from those containing food coloring.",
                  "role": "assistant",
                  "tool_calls": null,
                  "graph_data": {
                      "sources": [
                          {
                              "file_id": "1234",
                              "snippet": "with cocoa for an extra touch of chocolate…"
                          },
                          {
                              "file_id": "5678",
                              "snippet": "Sugar, corn syrup, artificial flavors, food coloring…"
                          }
                      ],
                      "status": "finished",
                      "subqueries": [
                          {
                              "query": "Which of our products contain food coloring?",
                              "answer": "The products that contain food coloring are...",
                              "sources": [
                                  {
                                      "file_id": "1234",
                                      "snippet": "Sugar, citric acid, artificial flavors…"
                                  },
                                  {
                                      "file_id": "5678",
                                      "snippet": "Coffee, coconut milk, ice"
                                  }
                              ]
                          },
                          {
                              "query": "Which of our products contain chocolate?",
                              "answer": "Several products contain chocolate. These include…",
                              "sources": [
                                  {
                                      "file_id": "1234",
                                      "snippet": "with cocoa for an extra touch of chocolate…"
                                  }
                              ]
                          }
                      ]
                  }
              },
          }
      ]
      // Other fields omitted for brevity    
  }
  ```

  ```json non-streaming response [expandable] {12} theme={null}
  {
      "id": "1234",
      "object": "chat.completion",
      "choices": [
          {
              "index": 0,
              "finish_reason": "stop",
              "message": {
                  "content": "None of our products contain both chocolate and food coloring. The products containing chocolate are different from those containing food coloring.",
                  "role": "assistant",
                  "tool_calls": null,
                  "graph_data": {
                      "sources": [
                          {
                              "file_id": "1234",
                              "snippet": "with cocoa for an extra touch of chocolate…"
                          },
                          {
                              "file_id": "5678",
                              "snippet": "Sugar, corn syrup, artificial flavors, food coloring…"
                          }
                      ],
                      "status": "finished",
                      "subqueries": [
                          {
                              "query": "Which of our products contain food coloring?",
                              "answer": "The products that contain food coloring are...",
                              "sources": [
                                  {
                                      "file_id": "1234",
                                      "snippet": "Sugar, citric acid, artificial flavors…"
                                  },
                                  {
                                      "file_id": "5678",
                                      "snippet": "Coffee, coconut milk, ice"
                                  }
                              ]
                          },
                          {
                              "query": "Which of our products contain chocolate?",
                              "answer": "Several products contain chocolate. These include…",
                              "sources": [
                                  {
                                      "file_id": "1234",
                                      "snippet": "with cocoa for an extra touch of chocolate…"
                                  }
                              ]
                          }
                      ]
                  }
              },
          }
      ]
      // Other fields omitted for brevity    
  }
  ```
</CodeGroup>

## Usage example

The following example uses a hypothetical product information Knowledge Graph to answer a question about which food products contain both food coloring and chocolate.

### Create the tools array

First, define the `tools` array with the `type` set to `graph`. The `function` object contains the `graph_ids`, `description`, and `subqueries` parameters.

In this example, the subqueries are included in the response. Subqueries can be useful for debugging or for providing additional context to the user about how the model arrived at the answer.

<CodeGroup>
  ```bash cURL theme={null}
  "tools": [
      {
          "type": "graph",
          "function": {
              "description": "Knowledge Graph containing information about Acme Inc. food products",
              "graph_ids": [
                  "<GRAPH_ID>"
              ],
              "subqueries": true
          }
      }
  ]
  ```

  ```python Python theme={null}
  tools = [{
      "type": "graph",
      "function": {
          "description": "Knowledge Graph containing information about Acme Inc. food products",
          "graph_ids": [
              "<GRAPH_ID>"
          ],
          "subqueries": True
      }
  }]
  ```

  ```javascript JavaScript theme={null}
  const tools = [{
      type: "graph",
      function: {
          description: "Knowledge Graph containing information about Acme Inc. food products",
          graph_ids: [
              "<GRAPH_ID>"
          ],
          subqueries: true
      }
  }]
  ```
</CodeGroup>

### Send the request

Add the tools array to the chat endpoint call along with your array of messages. Setting `tool_choice` to `auto` allows the model to choose when to use the Knowledge Graph tool, based on the user's question and the description of the tool.

This example streams the response in real-time as the model generates it.

If you are unfamiliar with the chat completions endpoint or streaming vs. non-streaming responses, learn more in the [chat completion guide](/home/chat-completion).

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "model": "palmyra-x5",
          "messages": [
              {
                  "role": "user",
                  "content": "Which of our products contain both food coloring and chocolate?"
              }
          ],
          "tool_choice": "auto",
          "tools": [
              {
                  "type": "graph",
                  "function": {
                      "description": "Knowledge Graph containing information about Acme Inc. food products",
                      "graph_ids": [
                          "<GRAPH_ID>"
                      ],
                      "subqueries": true
                  }
              }
          ],
          "stream": true
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  tools = [{
      "type": "graph",
      "function": {
          "description": "Knowledge Graph containing information about Acme Inc. food products",
          "graph_ids": [
              "<GRAPH_ID>"
          ],
          "subqueries": True
      }
  }]

  messages = [{"role": "user", "content": "Which of our products contain both food coloring and chocolate?"}]

  response = client.chat.chat(
      model="palmyra-x5", 
      messages=messages, 
      tools=tools,  # The tools array defined earlier.
      tool_choice="auto",
      stream=True
  )

  for chunk in response:
      if chunk.choices[0].delta.content is not None:
          print(chunk.choices[0].delta.content, end="", flush=True)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const tools = [{
      type: "graph",
      function: {
          description: "Knowledge Graph containing information about Acme Inc. food products",
          graph_ids: [
              "<GRAPH_ID>"
          ],
          subqueries: true
      }
  }]

  let messages = [{role: "user", content: "Which of our products contain both food coloring and chocolate?"}]

  const response = await client.chat.chat({
      model: "palmyra-x5", 
      messages: messages, 
      tools: tools, // The tools array defined earlier.
      tool_choice: "auto",
      stream: true 
  });

  for await (const chunk of response) {
      if (chunk.choices[0].delta.content) {
          process.stdout.write(chunk.choices[0].delta.content);
      }
  }
  ```
</CodeGroup>

### Display Knowledge Graph subqueries and sources

You may want to display the `sources` or `subqueries` in your UI to assist your user in understanding how the model derived the answer to the question. The following example shows how to display the subqueries as well as the status of the query from the Knowledge Graph.

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client.
  client = Writer()

  tools = [{
      "type": "graph",
      "function": {
          "description": "Knowledge Graph containing information about Acme Inc. food products",
          "graph_ids": [
              "<GRAPH_ID>"
          ],
          "subqueries": True
      }
  }]

  messages = [{"role": "user", "content": "Which of our products contain both food coloring and chocolate?"}]

  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto",
      stream=True
  )

  for chunk in response:
      if chunk.choices[0].delta.content is not None:
          print(chunk.choices[0].delta.content, end="", flush=True)

      if chunk.choices[0].delta.graph_data is not None:
          if chunk.choices[0].delta.graph_data.status is not None:
              print(f"Query status: {chunk.choices[0].delta.graph_data.status}")
          if chunk.choices[0].delta.graph_data.subqueries:
              print(f"Subquery: {chunk.choices[0].delta.graph_data.subqueries[0].query}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the client.
  const client = new Writer();

  const tools = [{
      type: "graph",
      function: {
          description: "Knowledge Graph containing information about Acme Inc. food products",
          graph_ids: [
              "<GRAPH_ID>"
          ],
          subqueries: true
      }
  }]

  let messages = [{role: "user", content: "Which of our products contain both food coloring and chocolate?"}]

  const response = await client.chat.chat({
      model: "palmyra-x5", 
      messages: messages, 
      tools: tools, // The tools array defined earlier.
      tool_choice: "auto",
      stream: true 
  });

  for await (const chunk of response) {
      if (chunk.choices[0].delta.content) {
          console.log(chunk.choices[0].delta.content);
      }

      if (chunk.choices[0].delta.graph_data) {
          if (chunk.choices[0].delta.graph_data.status) {
              console.log(`Query status: ${chunk.choices[0].delta.graph_data.status}`);
          }
          if (chunk.choices[0].delta.graph_data.subqueries && 
              chunk.choices[0].delta.graph_data.subqueries.length > 0 &&
              chunk.choices[0].delta.graph_data.subqueries[0].query) {
              console.log(`Subquery: ${chunk.choices[0].delta.graph_data.subqueries[0].query}`);
          }
      }
  }
  ```
</CodeGroup>

### Customize query behavior

You can fine-tune how your Knowledge Graph searches and retrieves content using the `query_config` parameter. Learn about all available configuration options in the [Knowledge Graph query configuration guide](/home/knowledge-graph-query-config).

## Next steps

By following this guide, you can reference Knowledge Graphs in your users' chats in your application.

Next, explore other ways to extend your chat applications:

* [Analyze images in a chat](/home/chat-with-images)
* [Pass questions to another model](/home/model-delegation)
* [Search the web](/home/web-search-tool)
* [Translate text](/home/translation-tool)


# Ask questions to Knowledge Graphs
Source: https://dev.writer.com/home/kg-query



The Knowledge Graph [question endpoint](/api-reference/kg-api/question) allows you to ask questions directly to one or more Knowledge Graphs. This provides a direct way to query your Knowledge Graphs without needing to use chat completions. You can also enable subqueries to break down complex questions into smaller subqueries and provide answers for each one.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Question endpoint

**Endpoint:** `POST /v1/graphs/question`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST https://api.writer.com/v1/graphs/question \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-raw '{
      "graph_ids": ["<GRAPH_ID>"],
      "question": "<QUESTION>"
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.graphs.question(
    graph_ids=["<GRAPH_ID>"],
    question="<QUESTION>"
  )

  print(f"Answer: {response.answer}")
  print(f"Sources: {response.sources}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.graphs.question({
    graph_ids: ["<GRAPH_ID>"],
    question: "<QUESTION>"
  });

  console.log(`Answer: ${response.answer}`);
  console.log(`Sources: ${response.sources}`);
  ```
</CodeGroup>

### Request body

The request body is a JSON object with the following fields:

| Parameter      | Type           | Required | Description                                                                                                                                                                                                                            |
| -------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `graph_ids`    | array\[string] | Yes      | The unique identifiers of the Knowledge Graphs to query. You can specify multiple Knowledge Graph IDs to search across multiple graphs.                                                                                                |
| `question`     | string         | Yes      | The question to answer using the Knowledge Graph.                                                                                                                                                                                      |
| `subqueries`   | Boolean        | No       | Specify whether to include subqueries. Defaults to `false`.                                                                                                                                                                            |
| `stream`       | Boolean        | No       | Determines whether to stream the response. If `true`, the output is sent as it is generated, which can be useful for real-time applications. Defaults to `false`. See how to [handle streaming responses](#stream-the-response) below. |
| `query_config` | object         | No       | Configuration options for Knowledge Graph queries. See [Query configuration parameters](#query-configuration-parameters) below for detailed parameter descriptions.                                                                    |

<Note>
  Find your Knowledge Graph ID using one of these methods:

  * Call the [Knowledge Graph list endpoint](/api-reference/kg-api/list-graphs) to retrieve all Knowledge Graphs with their IDs.
  * Locate the ID in the URL of the Knowledge Graph page in [AI Studio](https://app.writer.com/aistudio).

  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>

### Query configuration parameters

| Parameter            | Type    | Range              | Default | Description                                                                                                                                                                                                                                                                                                                                                                      |
| -------------------- | ------- | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `max_subquestions`   | integer | 1-10               | 6       | Maximum number of sub-questions to generate when processing complex queries.                                                                                                                                                                                                                                                                                                     |
| `search_weight`      | integer | 0-100              | 50      | Controls the balance between keyword and semantic search in ranking results.                                                                                                                                                                                                                                                                                                     |
| `grounding_level`    | number  | 0.0-1.0            | 0.0     | Controls how closely responses must match to source material. Set lower for grounded outputs, higher for creativity.                                                                                                                                                                                                                                                             |
| `max_snippets`       | integer | 5-25 (recommended) | 30      | Maximum number of text snippets to retrieve from the Knowledge Graph for context. Works in concert with `search_weight` to control best matches vs broader coverage. **Note**: While technically supports 1-60, values below 5 may return no results due to RAG implementation. Recommended range is 5-25. Due to RAG system behavior, you may see more snippets than requested. |
| `max_tokens`         | integer | 100-8000           | 4000    | Maximum number of tokens the model can generate in the response.                                                                                                                                                                                                                                                                                                                 |
| `keyword_threshold`  | number  | 0.0-1.0            | 0.7     | Threshold for keyword-based matching when searching Knowledge Graph content.                                                                                                                                                                                                                                                                                                     |
| `semantic_threshold` | number  | 0.0-1.0            | 0.7     | Threshold for semantic similarity matching when searching Knowledge Graph content. Set higher for stricter relevance, lower for broader range.                                                                                                                                                                                                                                   |
| `inline_citations`   | Boolean | true/false         | false   | Whether to include inline citations within the response text.                                                                                                                                                                                                                                                                                                                    |

For detailed explanations and usage examples, see the [Knowledge Graph query configuration guide](/home/knowledge-graph-query-config).

### Response format

The response is a JSON object with the following fields:

| Parameter              | Type   | Description                                                                                                 |
| ---------------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| `question`             | string | The question you asked.                                                                                     |
| `answer`               | string | The answer to the question based on the Knowledge Graph content.                                            |
| `sources`              | array  | An array of source objects that contain the file information and text snippets used to generate the answer. |
| `sources[].file_id`    | string | If the source is a file, this is the unique identifier of the file that contains the source information.    |
| `sources[].snippet`    | string | If the source is a file, this is a snippet of text from the source file used to answer the question.        |
| `subqueries`           | array  | An array of subquery objects. Only included if `subqueries` is `true`.                                      |
| `subqueries[].query`   | string | The subquery the model generated to answer the question.                                                    |
| `subqueries[].answer`  | string | The answer to the subquery.                                                                                 |
| `subqueries[].sources` | array  | An array of source objects for the subquery.                                                                |

<Tabs>
  <Tab title="Non-streaming response">
    ```json theme={null}
    {
      "question": "What was our company's revenue growth in Q4 2023?",
      "answer": "The company achieved 23% year-over-year revenue growth in Q4 2023, reaching $142 million in total revenue.",
      "sources": [
        {
          "file_id": "1234",
          "snippet": "In the fourth quarter of 2023, we saw strong performance with revenue reaching $142 million, representing a 23% increase compared to Q4 2022."
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Streaming response">
    ```json theme={null}
    data: {
        "question": "What was our company's revenue growth in Q4 2023?",
        "answer": "The",
        "sources": [],
        "subqueries": null,
        "references": null
    }
    ```
  </Tab>
</Tabs>

## Examples

### Query multiple knowledge graphs

You can query multiple Knowledge Graphs at once by providing multiple graph IDs in the `graph_ids` array.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST https://api.writer.com/v1/graphs/question \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-raw '{
      "graph_ids": [
        "<GRAPH_ID_1>",
        "<GRAPH_ID_2>"
      ],
      "question": "What are the key findings from our research documents?"
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  client = Writer()

  response = client.graphs.question(
    graph_ids=[
      "<GRAPH_ID_1>",
      "<GRAPH_ID_2>"
    ],
    question="What are the key findings from our research documents?"
  )

  print(f"Answer: {response.answer}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  const client = new Writer();

  const response = await client.graphs.question({
    graph_ids: [
      "<GRAPH_ID_1>",
      "<GRAPH_ID_2>"
    ],
    question: "What are the key findings from our research documents?"
  });

  console.log(`Answer: ${response.answer}`);
  ```
</CodeGroup>

### Enable subqueries

When you enable subqueries, the model breaks down complex questions into smaller subqueries and provides answers for each one from the Knowledge Graphs.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST https://api.writer.com/v1/graphs/question \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-raw '{
      "graph_ids": ["<GRAPH_ID>"],
      "question": "What are the main products and their market performance?",
      "subqueries": true
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  client = Writer()

  response = client.graphs.question(
    graph_ids=["<GRAPH_ID>"],
    question="What are the main products and their market performance?",
    subqueries=True
  )

  print(f"Main answer: {response.answer}")
  for subquery in response.subqueries:
      print(f"Subquery: {subquery.query}")
      print(f"Answer: {subquery.answer}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  const client = new Writer();

  const response = await client.graphs.question({
    graph_ids: ["<GRAPH_ID>"],
    question: "What are the main products and their market performance?",
    subqueries: true
  });

  console.log(`Main answer: ${response.answer}`);
  response.subqueries.forEach(subquery => {
      console.log(`Subquery: ${subquery.query}`);
      console.log(`Answer: ${subquery.answer}`);
  });
  ```
</CodeGroup>

### Stream the response

You can stream the response from the Knowledge Graphs by setting the `stream` parameter to `true`. This is useful for real-time applications that need to see the response as it is generated.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST https://api.writer.com/v1/graphs/question \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-raw '{
      "graph_ids": ["<GRAPH_ID>"],
      "question": "<QUESTION>",
      "stream": true
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  client = Writer()

  response = client.graphs.question(
    graph_ids=["<GRAPH_ID>"],
    question="<QUESTION>",
    stream=True
  )

  for chunk in response:
      print(chunk.answer, end="", flush=True)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  const client = new Writer();

  const response = await client.graphs.question({
    graph_ids: ["<GRAPH_ID>"],
    question: "<QUESTION>",
    stream: true
  });

  for await (const chunk of response) {
      process.stdout.write(chunk.answer);
  }
  ```
</CodeGroup>

## Next steps

* Learn how to [create and manage Knowledge Graphs](/home/knowledge-graph)
* Use Knowledge Graphs in chat completions with [tool calling](/home/kg-chat)
* Explore [Knowledge Graph API reference](/api-reference/kg-api)


# Create and manage a Knowledge Graph
Source: https://dev.writer.com/home/knowledge-graph

Integrate graph-based RAG into your agents with Knowledge Graph API. Achieve higher accuracy than traditional vector-based retrieval methods.

Knowledge Graph, our graph-based retrieval-augmented generation (RAG), achieves [higher accuracy](https://arxiv.org/abs/2405.02048) than traditional RAG approaches that use vector retrieval.

This guide will help you understand and use the [Knowledge Graph API](/api-reference/kg-api) to integrate RAG capabilities into your agents.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>

## Create a Knowledge Graph

A Knowledge Graph is a collection of files that are used to answer questions. To start working with a Knowledge Graph, create an empty Knowledge Graph that you can add files to.

**Endpoint**: `POST https://api.writer.com/v1/graphs`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST https://api.writer.com/v1/graphs \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-raw '{
      "name": "Financial Reports",
      "description": "Knowledge Graph of 2024 financial reports"
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  graph_create_response = client.graphs.create(
      name="Financial Reports",
      description="Knowledge Graph of 2024 financial reports"
  )
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const graphCreateResponse = await client.graphs.create({
    name: "Financial Reports",
    description: "Knowledge Graph of 2024 financial reports"
  });
  ```
</CodeGroup>

### Request body

The request body is a JSON object that contains the following fields:

| Parameter     | Type   | Description                                                                                                                                             |
| ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | string | The name of the Knowledge Graph.                                                                                                                        |
| `description` | string | The description of the Knowledge Graph. The description should help the model understand the purpose of the Knowledge Graph and when it should be used. |

### Response format

The response has the following structure:

| Parameter     | Type   | Description                                        |
| ------------- | ------ | -------------------------------------------------- |
| `id`          | string | The ID of the Knowledge Graph.                     |
| `created_at`  | string | The date and time the Knowledge Graph was created. |
| `name`        | string | The name of the Knowledge Graph.                   |
| `description` | string | The description of the Knowledge Graph.            |

```json theme={null}
{
  "id": "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a",
  "created_at": "2024-06-24T12:34:56Z",
  "name": "Financial Reports",
  "description": "Knowledge Graph of 2024 financial reports"
}
```

<Note>
  Find your Knowledge Graph ID using one of these methods:

  * Call the [Knowledge Graph list endpoint](/api-reference/kg-api/list-graphs) to retrieve all Knowledge Graphs with their IDs.
  * Locate the ID in the URL of the Knowledge Graph page in [AI Studio](https://app.writer.com/aistudio).
</Note>

## Add a file to a Knowledge Graph

Once you've created a Knowledge Graph, you can add files to it. The files you add to a Knowledge Graph are used to answer questions and enhance the accuracy of the responses from the LLM

You must upload the file to the Writer API before adding it to a Knowledge Graph. See [Manage files](/home/files) for more information about how to upload files.

**Endpoint**: `POST /v1/graphs/{graph_id}/file`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST https://api.writer.com/v1/graphs/{graph_id}/file \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --header "Content-Type: application/json" \
  --data-raw '{"file_id":"1862f090-a281-48f3-8838-26c1e78b605e"}'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  graph_file = client.graphs.add_file_to_graph(
    graph_id="{graph_id}",
    file_id="1862f090-a281-48f3-8838-26c1e78b605e",
  )
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const file = await client.graphs.addFileToGraph('{graph_id}', {
    file_id: '1862f090-a281-48f3-8838-26c1e78b605e',
  });
  ```
</CodeGroup>

The response will have this structure:

```json theme={null}
{
  "id": "1862f090-a281-48f3-8838-26c1e78b605e",
  "created_at": "2024-07-01T20:41:44.159505Z",
  "name": "test.txt",
  "graph_ids": [
    "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a"
  ]
}
```

### Path parameters

| Parameter  | Description                              |
| ---------- | ---------------------------------------- |
| `graph_id` | The ID of the Knowledge Graph to update. |

### Request body

The request body is a JSON object that contains the following fields:

| Parameter | Description                                       |
| --------- | ------------------------------------------------- |
| `file_id` | The ID of the file to add to the Knowledge Graph. |

You must upload the file to the Writer API before adding it to a Knowledge Graph. See [Manage files](/home/files) for more information about how to upload files.

## Remove a file from a Knowledge Graph

**Endpoint**: `DELETE /v1/graphs/{graph_id}/file/{file_id}`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request DELETE "https://api.writer.com/v1/graphs/{graph_id}/file/{file_id}" \
  --header "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  client.graphs.remove_file_from_graph(
      graph_id="{graph_id}",
      file_id="{file_id}"
  )
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const file = await client.graphs.removeFileFromGraph('{graph_id}', '{file_id}');
  ```
</CodeGroup>

### Path parameters

| Parameter  | Description                                            |
| ---------- | ------------------------------------------------------ |
| `graph_id` | The ID of the Knowledge Graph to update.               |
| `file_id`  | The ID of the file to remove from the Knowledge Graph. |

### Response format

The response has this structure:

```json theme={null}
{
  "id": "1862f090-a281-48f3-8838-26c1e78b605e",
  "deleted": true
}
```

## Add URLs to a Knowledge Graph

You can add URLs to a Knowledge Graph to enhance the model's knowledge with content from websites. The LLM can then query and use this information when responding to questions.

This example adds a web connector URL to a Knowledge Graph. It first retrieves the Knowledge Graph to see the current URLs and then adds a new URL to the Knowledge Graph. Note that this operation replaces the entire list of URLs attached to the Knowledge Graph, which is why it is important to retrieve the existing list of URLs first.

<CodeGroup>
  ```bash cURL theme={null}
  # 1. First, retrieve the current Knowledge Graph to see existing URLs
  curl --location 'https://api.writer.com/v1/graphs/{graph_id}' \
      --header "Authorization: Bearer $WRITER_API_KEY"

  # 2. Then, send a new request with all the URLs you want to add. In this example, there were no existing URLs, so it's adding a single URL.
  curl --location --request PUT 'https://api.writer.com/v1/graphs/{graph_id}' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "urls": [
              {
                  "url": "https://example.com/documentation",
                  "type": "sub_pages",
                  "exclude_urls": [
                      "https://example.com/documentation/admin",
                      "https://example.com/documentation/private"
                  ]
              }
          ]
      }'
  ```

  ```python Python theme={null}

  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  graph_id = "GRAPH_ID"

  graph = client.graphs.retrieve(graph_id)
  urls = graph.urls

  urls.append({
      "url": "https://example.com/documentation",
      "type": "sub_pages",
      "exclude_urls": ["https://example.com/documentation/admin", "https://example.com/documentation/private"]
  })

  client.graphs.update(graph_id, urls=urls)

  print(client.graphs.retrieve(graph_id).urls)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const graph_id = "GRAPH_ID";

  const graph = await client.graphs.retrieve(graph_id);

  const urls = graph.urls;

  urls.push({
    url: 'https://example.com/documentation',
    type: 'sub_pages',
    exclude_urls: [
      'https://example.com/documentation/admin',
      'https://example.com/documentation/private'
    ]
  });

  const response = await client.graphs.update(graph_id, { urls });

  const updatedGraph = await client.graphs.retrieve(graph_id);

  console.log(updatedGraph.urls);
  ```
</CodeGroup>

See more examples in [Add URLs to a Knowledge Graph](/home/web-connector-url).

## Add a Knowledge Graph to a no-code agent

After you've created a Knowledge Graph, you can add it to a [no-code agent](/home/applications) to add RAG capabilities.

You can use this endpoint to add or remove Knowledge Graphs from a no-code agent.

**Endpoint**: `PUT /v1/applications/{application_id}/graphs`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request PUT "https://api.writer.com/v1/applications/{application_id}/graphs" \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --header "Content-Type: application/json" \
  --data-raw '{"graph_id":"6029b226-1ee0-4239-a1b0-cdeebfa3ad5a"}'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  client.applications.graphs.update(
      application_id="{application_id}",
      graph_ids=["{graph_id}"]
  )
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  client.applications.graphs.update('{application_id}', ['{graph_id}']);
  ```
</CodeGroup>

### Path parameters

| Parameter        | Description                            |
| ---------------- | -------------------------------------- |
| `application_id` | The ID of the no-code agent to update. |

### Request body

The request body is a JSON object that contains the following fields:

| Parameter   | Type           | Description                                                          |
| ----------- | -------------- | -------------------------------------------------------------------- |
| `graph_ids` | array\[string] | The IDs of the Knowledge Graphs to associate with the no-code agent. |

This method updates the associated Knowledge Graphs to the exact list of IDs provided in the request.

To remove a single Knowledge Graph from the no-code agent, set the `graph_ids` parameter to an array containing the IDs of all the other Knowledge Graphs associated with the no-code agent, excluding the ID of the Knowledge Graph to remove. To remove all Knowledge Graphs from the no-code agent, set this parameter to an empty array.

### Response format

The response contains the new list of IDs of the Knowledge Graphs associated with the no-code agent.

```json theme={null}
{
  "graph_ids": [
    "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a"
  ]
}
```

## Next steps

* Learn how to ask your Knowledge Graph questions in a chat completion via [tool calling](/home/kg-chat)
* Query your Knowledge Graph directly using the [question endpoint](/home/kg-query)
* Fine-tune how your Knowledge Graph searches and retrieves content using the `query_config` parameter in the [Knowledge Graph query configuration guide](/home/knowledge-graph-query-config)


# Connect your data to AI with Knowledge Graph
Source: https://dev.writer.com/home/knowledge-graph-concepts

Understand graph-based RAG with Knowledge Graph. Learn how content ingestion, graph construction, and query processing deliver accurate AI responses.

Knowledge Graph is Writer's retrieval-augmented generation (RAG) system that uses graph-based relationships that can improve accuracy compared to vector-only approaches ([reference](https://arxiv.org/abs/2405.02048)). Connect your data sources to enable LLMs to access, understand, and reason about your specific information when generating responses. Get inline citations to ensure every answer pulls from your actual data.

## Knowledge Graph concepts

Knowledge Graph is a structured way to store and retrieve information from your documents, websites, and data sources. Unlike traditional search systems that rely on keyword matching or vector similarity, Knowledge Graph creates a network of interconnected information that captures relationships between concepts, entities, and ideas.

### How Knowledge Graph works

Knowledge Graph processes your data through several stages:

1. **Content ingestion**: upload files, connect data sources, or add website URLs
2. **Graph construction**: analyze content to identify entities, concepts, and relationships
3. **Indexing**: create a searchable graph structure that captures semantic connections
4. **Query processing**: when you ask questions, the system traverses the graph to find relevant information
5. **Response generation**: combine retrieved information with AI to generate accurate, contextual answers

<img alt="Knowledge Graph flow from ingestion to graph construction, indexing, query processing, and response generation." />

## Data sources and formats

Knowledge Graph supports multiple ways to add data:

| Source type             | Description                   | Supported formats/features                                                                                                                                                                                   | Learn more                                                                                                    |
| ----------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| **File uploads**        | Upload documents directly     | - PDF, TXT, DOC/DOCX, PPT/PPTX<br />- CSV, XLS/XLSX<br />- EML, HTML, SRT                                                                                                                                    | [Manage Knowledge Graph data](/home/knowledge-graph)                                                          |
| **Data connectors**     | Connect to external platforms | - Confluence: Access team documentation and wikis<br />- Notion: Import workspaces and databases<br />- Google Drive: Connect to shared drives and documents<br />- SharePoint: Access Microsoft 365 content | [Data connectors guide](https://support.writer.com/article/251-setting-up-knowledge-graph-data-connectorss)   |
| **Website integration** | Add web content               | - Add specific URLs or entire domains<br />- Configure page inclusion/exclusion<br />- Automatic content updates                                                                                             | [Website integration guide](https://support.writer.com/article/272-setting-up-knowledge-graph-web-connectors) |

Learn more about [data sources and formats](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph).

## Query capabilities

Knowledge Graph supports natural language questions and querying across multiple graphs. You can configure the retrieval to break it down into subqueries, support streaming, get inline citations, and fine-tune the search parameters.

**Core features:**

* **Natural language questions**: ask questions in plain, conversational language without needing special syntax or query formats. For example, "What is our refund policy?" or "Tell me about our enterprise pricing tiers" will work naturally.
* **Multi-graph queries**: search across multiple Knowledge Graphs simultaneously. For example, you can compare data across departments or time periods.

**Configurable options:**

* **Subqueries**: break down complex questions into smaller parts automatically
* **Streaming responses**: get answers as they're generated for real-time applications
* **Inline citations**: get answers with embedded source references to verify information
* **Query configuration**: fine-tune search parameters including search weight, grounding level, maximum snippets and tokens, and semantic thresholds

## Add Knowledge Graph to your Writer applications

Add Knowledge Graph to your Writer applications to create conversational AI, generate content, and enable tool calling in complex workflows. Get started with these guides:

* [Create and manage Knowledge Graphs](/home/knowledge-graph): set up your first Knowledge Graph via the Writer API and SDKs
* [Ask questions to Knowledge Graphs](/home/kg-query): query your data directly via the Writer API and SDKs
* [Build chat applications with Knowledge Graph](/no-code/chat-kg): create conversational AI that can reference your specific data
* [Use Knowledge Graph with tool calling](/agent-builder/tool-calling-intro): integrate Knowledge Graph with Agent Builder
* [Enable inline citations](/home/inline-citations): get source references in responses from Knowledge Graph queries
* [Knowledge Graph query configuration](/home/knowledge-graph-query-config): fine-tune search parameters

## Best practices

### Organize your data

* Create separate Knowledge Graphs for different topics or departments
* Use descriptive names and descriptions to help the AI understand context
* Regularly update content to ensure accuracy

### Optimize queries

* Ask specific, well-formed questions for better results
* Use the query configuration parameters to fine-tune responses
* Enable subqueries for complex multi-part questions

### Monitor and improve

* Review source citations to understand how information is being retrieved
* Update Knowledge Graphs with new information as it becomes available
* Test different query approaches to find what works best for your use case

## Next steps

* Learn how to [create and manage Knowledge Graphs](/home/knowledge-graph)
* Explore the [Knowledge Graph API reference](/api-reference/kg-api)
* See how to [ask questions to Knowledge Graphs](/home/kg-query)
* Understand [Knowledge Graph query configuration](/home/knowledge-graph-query-config)
* For additional help, visit the [Writer Help Center](https://support.writer.com) for detailed guides and troubleshooting


# Configure Knowledge Graph query parameters
Source: https://dev.writer.com/home/knowledge-graph-query-config



This guide shows you how to customize Knowledge Graph query behavior using the `query_config` parameter. You can tune search algorithms, control response grounding, adjust content retrieval, and enable inline citations to configure Knowledge Graph responses for your specific use cases.

## Overview

The `query_config` parameter allows you to fine-tune how Knowledge Graphs search, rank, and retrieve content. You can control the balance between keyword and semantic search, adjust how closely responses match source material, set relevance thresholds, and enable inline citations for source verification.

You can use this parameter with:

* **Chat completions** with [Knowledge Graph tools](/home/kg-chat)
* **Direct Knowledge Graph queries** via the [`/v1/graphs/question`](/home/kg-query) endpoint

## Example

Here's a chat completion example that demonstrates various query configuration parameters to configure Knowledge Graph responses:

* `grounding_level: 0.2`: keeps responses closely tied to source material (20% creative interpretation allowed)
* `search_weight: 60`: balances keyword and semantic search (60% keyword search, 40% semantic search)
* `keyword_threshold: 0.6`: requires 60% keyword match for content to be included
* `semantic_threshold: 0.8`: requires 80% semantic similarity for content to be included

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST 'https://api.writer.com/v1/chat' \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header 'Content-Type: application/json' \
    --data-raw '{
      "model": "palmyra-x5",
      "messages": [
        {
          "role": "user",
          "content": "What are the key features of our product?"
        }
      ],
      "tools": [
        {
          "type": "graph",
          "function": {
            "description": "Search company knowledge base",
            "graph_ids": ["<GRAPH_ID>"],
            "subqueries": true,
            "query_config": {
              "grounding_level": 0.2,
              "search_weight": 60,
              "keyword_threshold": 0.6,
              "semantic_threshold": 0.8,
              "inline_citations": true
            }
          }
        }
      ]
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client
  client = Writer()

  # Configure Knowledge Graph tool with query parameters
  tools = [{
      "type": "graph",
      "function": {
          "description": "Search company knowledge base",
          "graph_ids": ["<GRAPH_ID>"],
          "subqueries": True,
          "query_config": {
              "grounding_level": 0.2,
              "search_weight": 60,
              "keyword_threshold": 0.6,
              "semantic_threshold": 0.8,
              "inline_citations": True
          }
      }
  }]

  messages = [{"role": "user", "content": "What are the key features of our product?"}]

  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto"
  )

  print(response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client
  const client = new Writer();

  // Configure Knowledge Graph tool with query parameters
  const tools = [{
      type: "graph",
      function: {
          description: "Search company knowledge base",
          graph_ids: ["<GRAPH_ID>"],
          subqueries: true,
          query_config: {
              grounding_level: 0.2,
              search_weight: 60,
              keyword_threshold: 0.6,
              semantic_threshold: 0.8,
              inline_citations: true
          }
      }
  }];

  const messages = [{ role: "user", content: "What are the key features of our product?" }];

  const response = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      tools: tools,
      tool_choice: "auto"
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

<Note>
  Find your Knowledge Graph ID using one of these methods:

  * Call the [Knowledge Graph list endpoint](/api-reference/kg-api/list-graphs) to retrieve all Knowledge Graphs with their IDs.
  * Locate the ID in the URL of the Knowledge Graph page in [AI Studio](https://app.writer.com/aistudio).

  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>

## Configure request parameters

| Name                 | Type    | Range              | Default | Description                                                                                                                                                                                                                                                                                                                                               |
| -------------------- | ------- | ------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `max_subquestions`   | Integer | 1-10               | 6       | Maximum number of sub-questions to generate when processing complex queries. Set higher to improve detail, set lower to reduce response time. See [Max sub-questions](#max-sub-questions) for details.                                                                                                                                                    |
| `search_weight`      | Integer | 0-100              | 50      | Controls the balance between keyword and semantic search in ranking results. See [Search weight](#search-weight) for details.                                                                                                                                                                                                                             |
| `grounding_level`    | Number  | 0.0-1.0            | 0.0     | Controls how closely responses must match to source material. Set lower for grounded outputs, higher for creativity. See [Grounding level](#grounding-level) for details.                                                                                                                                                                                 |
| `max_snippets`       | Integer | 5-25 (recommended) | 30      | Maximum number of text snippets to retrieve from the Knowledge Graph for context. Works in concert with `search_weight` to control best matches vs broader coverage. **Note**: While technically supports 1-60, values below 5 may return no results due to RAG implementation. Recommended range is 5-25. See [Max snippets](#max-snippets) for details. |
| `max_tokens`         | Integer | 100-8000           | 4000    | Maximum number of tokens the model can generate in the response. See [Max tokens](#max-tokens) for details.                                                                                                                                                                                                                                               |
| `keyword_threshold`  | Number  | 0.0-1.0            | 0.7     | Threshold for keyword-based matching when searching Knowledge Graph content. Set higher for stricter relevance, lower for broader range. See [Keyword threshold](#keyword-threshold) for details.                                                                                                                                                         |
| `semantic_threshold` | Number  | 0.0-1.0            | 0.7     | Threshold for semantic similarity matching when searching Knowledge Graph content. Set higher for stricter relevance, lower for broader range. See [Semantic threshold](#semantic-threshold) for details.                                                                                                                                                 |
| `inline_citations`   | Boolean | True/False         | False   | Whether to include inline citations within the response text. This is only available when making direct Knowledge Graph queries via the `/v1/graphs/question` endpoint. See [Work with inline citations in Knowledge Graph responses](/home/inline-citations) for details.                                                                                |

## Parameter details

### Inline citations

When you enable `inline_citations: true`, the response includes source references directly in the text as `[filename.pdf](cite_id)` links. These citations correspond to entries in the `references` array, allowing you to verify information and trace claims back to their origins.

**Key features**:

* **Source verification**: Each citation links to specific source material
* **Transparency**: Users can see exactly which sources support each claim
* **Traceability**: Citations include unique identifiers for programmatic processing

For detailed information about working with inline citations, see [Work with inline citations in Knowledge Graph responses](/home/inline-citations).

### Max sub-questions

Maximum number of sub-questions to generate when processing complex queries. Higher values allow the system to break down complex questions into more detailed sub-queries.

**How it works**:

* **Higher values**: Improve detail by generating more sub-questions for thorough analysis
* **Lower values**: Reduce response time by generating fewer sub-questions

**When to adjust**:

* **Increase** for complex, multi-part questions that need thorough analysis
* **Decrease** for simple, direct questions to reduce processing time

### Search weight

Controls the balance between keyword and semantic search in ranking results.

**How it works**:

* **Higher values (closer to 100)**: Prioritize keyword-based matching
* **Lower values (closer to 0)**: Prioritize semantic similarity matching

**When to adjust**:

* **Increase** for searches where exact keyword matches matter most
* **Decrease** for searches where conceptual similarity is more important

### Grounding level

Controls how closely responses must be tied to source material. This is different from typical LLM temperature parameters. It specifically controls grounding to Knowledge Graph sources.

**How it works**:

* **Higher values**: Higher creativity - allow more creative interpretation of source material
* **Lower values (closer to 0.0)**: Grounded outputs - stick closely to source material with minimal creativity

**Examples**:

* `0.0`: "According to the documentation, the API supports JSON responses" (direct quote/paraphrase)
* `0.5`: "The API documentation indicates that JSON responses are supported, which suggests additional capabilities" (interpretive)
* `1.0`: "Based on the available information, users can expect JSON responses, though other formats might be possible" (highly interpretive)

**When to adjust**:

* **Increase** for higher creativity when you want more interpretive responses
* **Decrease** for grounded outputs when factual reporting or accuracy is critical

### Max snippets

Maximum number of text snippets to retrieve from the Knowledge Graph for context. Works in concert with `search_weight` to control best matches vs broader coverage.

**How it works**:

* **Lower values (5-15)**: Best matches - retrieve fewer, more relevant snippets
* **Higher values (15-25)**: Broader coverage - retrieve more snippets for comprehensive context
* **Values below 5**: May return no results due to RAG implementation limitations

**Important notes**:

* **Recommended range**: 5-25 (default is 30, which is higher than recommended)
* **Edge case**: Due to RAG system behavior, you may see more snippets than requested. Use the recommended range unless you have a measured need to change it.

**When to adjust**:

* **Increase** for broader coverage when you need comprehensive research or more context
* **Decrease** for best matches when you want focused queries or concise results

### Max tokens

Maximum number of tokens the model can generate in the response. This controls the length of the AI's answer.

**How it works**:

* **Higher values**: Allow longer, more detailed responses
* **Lower values**: Generate shorter, more concise responses

**When to adjust**:

* **Increase** for detailed analysis or comprehensive answers
* **Decrease** for quick summaries or when you need faster responses

### Keyword threshold

Threshold for keyword-based matching when searching Knowledge Graph content.

**How it works**:

* **Higher values**: Stricter relevance - require stronger keyword matches
* **Lower values**: Broader range - allow more lenient keyword matching

**When to adjust**:

* **Increase** for stricter relevance when you want very precise keyword matches
* **Decrease** for broader range when you want to include content with related but not exact keywords

### Semantic threshold

Threshold for semantic similarity matching when searching Knowledge Graph content.

**How it works**:

* **Higher values**: Stricter relevance - require stronger semantic similarity
* **Lower values**: Broader range - allow more lenient semantic matching

**Examples**:

* `0.9`: Very strict - only content with high semantic similarity (for example, searching "user authentication" only finds content about "user authentication", "login", "sign-in")
* `0.7`: Moderate - includes conceptually related content (for example, searching "user authentication" finds "authentication", "security", "access control", "user management")
* `0.3`: Lenient - includes tangentially related content (for example, searching "user authentication" finds "authentication", "security", "user management", "database", "API", "web development")

**When to adjust**:

* **Increase** for stricter relevance when you want very semantically similar content
* **Decrease** for broader range when you want to include tangentially related content

## Parameter interactions and performance

### How parameters work together

Some parameters interact in ways that affect both results and performance:

* **`max_snippets` and `search_weight`**: Work together to control best matches vs broader coverage. `max_snippets` controls how many snippets are fed to the LLM in RAG.
* **`max_snippets` and `max_tokens`**: `max_snippets` controls input context size, while `max_tokens` controls output response length.
* **`keyword_threshold` and `semantic_threshold`**: Both filters are applied - content must pass both thresholds to be included.
* **Search weight and thresholds**: Higher thresholds reduce the candidate pool, then search weight determines the balance between keyword and semantic ranking of remaining results.

#### Search weight versus semantic threshold

These two parameters work at different stages of the search process:

* Semantic threshold is applied during the initial search phase, before ranking
* Search weight is applied after filtering, when ranking the remaining results

Semantic threshold acts as a gatekeeper that determines which content gets included, while search weight controls the balance between keyword and semantic scoring in final ranking.

**Example**: with `semantic_threshold = 0.8` and `search_weight = 30`, you get a small set of very relevant documents ranked more by semantic similarity than keyword matching.

### Performance considerations

* **Higher `max_tokens`**: Increases processing time and cost
* **Lower thresholds**: May return more results but with lower relevance
* **Higher `max_subquestions`**: Increases processing time for complex queries
* **`inline_citations: true`**: Minimal performance impact, slight increase in response size

### Recommended starting values

For most use cases, use the default values and adjust only if you need specific behavior. The defaults are designed to work well for general Knowledge Graph queries.

## Usage examples

### Chat completions with Knowledge Graph tool

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST 'https://api.writer.com/v1/chat' \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header 'Content-Type: application/json' \
    --data-raw '{
      "model": "palmyra-x5",
      "messages": [
        {
          "role": "user",
          "content": "What are the key features of our product?"
        }
      ],
      "tools": [
        {
          "type": "graph",
          "function": {
            "description": "Search company knowledge base",
            "graph_ids": ["<GRAPH_ID>"],
            "subqueries": true,
            "query_config": {
              "grounding_level": 0.2,
              "search_weight": 60,
              "keyword_threshold": 0.6,
              "semantic_threshold": 0.8,
              "inline_citations": true
            }
          }
        }
      ]
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client
  client = Writer()

  # Configure Knowledge Graph tool with query parameters
  tools = [{
      "type": "graph",
      "function": {
          "description": "Search company knowledge base",
          "graph_ids": ["<GRAPH_ID>"],
          "subqueries": True,
          "query_config": {
              "grounding_level": 0.2,
              "search_weight": 60,
              "keyword_threshold": 0.6,
              "semantic_threshold": 0.8,
              "inline_citations": True
          }
      }
  }]

  messages = [{"role": "user", "content": "What are the key features of our product?"}]

  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto"
  )

  print(response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client
  const client = new Writer();

  // Configure Knowledge Graph tool with query parameters
  const tools = [{
      type: "graph",
      function: {
          description: "Search company knowledge base",
          graph_ids: ["<GRAPH_ID>"],
          subqueries: true,
          query_config: {
              grounding_level: 0.2,
              search_weight: 60,
              keyword_threshold: 0.6,
              semantic_threshold: 0.8,
              inline_citations: true
          }
      }
  }];

  const messages = [{ role: "user", content: "What are the key features of our product?" }];

  const response = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      tools: tools,
      tool_choice: "auto"
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

### Direct Knowledge Graph query

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request POST 'https://api.writer.com/v1/graphs/question' \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --header 'Content-Type: application/json' \
    --data-raw '{
      "graph_ids": ["<GRAPH_ID>"],
      "question": "What are the key features of our product?",
      "query_config": {
        "grounding_level": 0.2,
        "keyword_threshold": 0.6,
        "semantic_threshold": 0.8,
        "inline_citations": true
      }
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client
  client = Writer()

  # Query Knowledge Graph with configuration parameters
  response = client.graphs.question(
      graph_ids=["<GRAPH_ID>"],
      question="What are the key features of our product?",
      query_config={
          "grounding_level": 0.2,
          "keyword_threshold": 0.6,
          "semantic_threshold": 0.8,
          "inline_citations": True
      }
  )

  print(f"Answer: {response.answer}")
  print(f"References: {response.references}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the Writer client
  const client = new Writer();

  // Query Knowledge Graph with configuration parameters
  const response = await client.graphs.question({
      graph_ids: ["<GRAPH_ID>"],
      question: "What are the key features of our product?",
      query_config: {
          grounding_level: 0.2,
          keyword_threshold: 0.6,
          semantic_threshold: 0.8,
          inline_citations: true
      }
  });

  console.log(`Answer: ${response.answer}`);
  console.log(`References: ${response.references}`);
  ```
</CodeGroup>

## Common configuration patterns

### Research and analysis

Use this configuration for comprehensive research tasks where you need thorough analysis with source verification. Higher sub-questions and snippets provide more context, while inline citations help track sources.

```json theme={null}
{
  "max_subquestions": 8,
  "search_weight": 60,
  "grounding_level": 0.1,
  "max_snippets": 50,
  "max_tokens": 7000,
  "keyword_threshold": 0.6,
  "semantic_threshold": 0.7,
  "inline_citations": true
}
```

### Quick answers

Use this configuration for fast, focused responses where speed and precision matter more than comprehensive analysis. Strict thresholds ensure high relevance with minimal processing time.

```json theme={null}
{
  "max_subquestions": 3,
  "search_weight": 80,
  "grounding_level": 0.0,
  "max_snippets": 15,
  "max_tokens": 2000,
  "keyword_threshold": 0.8,
  "semantic_threshold": 0.8,
  "inline_citations": false
}
```

### Creative content generation

Use this configuration when you want the AI to interpret and build upon source material creatively. Lower thresholds allow more diverse content, while higher grounding level enables interpretive responses.

```json theme={null}
{
  "max_subquestions": 6,
  "search_weight": 40,
  "grounding_level": 0.6,
  "max_snippets": 30,
  "max_tokens": 5000,
  "keyword_threshold": 0.5,
  "semantic_threshold": 0.6,
  "inline_citations": false
}
```

## Best practices

1. **Start with defaults**: Begin with the default configuration and adjust based on your specific needs
2. **Test incrementally**: Change one parameter at a time to understand its effect
3. **Consider your use case**: Different applications, like research, Q\&A, and content generation, benefit from different configurations
4. **Monitor performance**: Track how different configurations affect response quality and processing time
5. **Balance precision and recall**: Higher thresholds give more precise results but may miss relevant content

## Next steps

* Learn how to [work with inline citations](/home/inline-citations) for source verification and traceability
* Explore [Knowledge Graph tools](/home/kg-chat) for chat completions
* Check out [direct Knowledge Graph queries](/home/kg-query) for programmatic access
* See [Knowledge Graph management](/home/knowledge-graph) for creating and organizing your graphs


# Create and manage Knowledge Graphs
Source: https://dev.writer.com/home/knowledge-graphs





# Language support
Source: https://dev.writer.com/home/language_support

Palmyra LLM supports 30+ languages including Arabic, French, Spanish, Hindi, and Chinese. View MMLU and BLEU benchmark scores for multilingual text generation.

Writer Palmyra LLM supports over 30 languages, including Arabic, French, Spanish, Hindi, Simplified Chinese, Traditional Chinese, and more. This page provides an overview of capabilities, performance benchmarks, and prompting examples on how to use these features.

When it comes to multi-language capabilities, there are two primary categories to consider: generation and translation. Generation typically refers to the ability to understand/create content, answer questions, and converse, all within the same language. Translation typically refers to the ability to transform text to and from English, where either the input or output language is English.

This page displays two of the many benchmarks used to evaluate multi-language performance in Palmyra LLMs. Writer Palmyra has the highest performance of any production LLM in the Holistic Evaluation of Language Models, an LLM evaluation framework developed by Stanford CRFM to serve as a living benchmark for the community, continuously updated with new scenarios, metrics, and models. While there are limited benchmarks available for evaluating text generation and translation in different languages, Palmyra has achieved some of the highest scores in both MMLU (Massive Multitask Language Understanding) and BLEU (Bilingual Evaluation Understudy) for other languages.

One benchmark that Writer uses to evaluate text generation performance is [MMLU](https://arxiv.org/abs/2009.03300). The [MLMM evaluation](https://arxiv.org/pdf/2307.16039.pdf) covers 57 tasks including elementary mathematics, U.S. history, computer science, law, and more. To attain high accuracy on this test, models must possess extensive world knowledge and problem-solving ability.

One benchmark that Writer uses to evaluate text translation performance is [BLEU](https://aclanthology.org/P02-1040.pdf). [Any BLEU score higher than 60](https://cloud.google.com/translate/automl/docs/evaluate#interpretation) indicates a higher quality translation than a human translation.

While Palmyra's core competency lies in the text generation realm, translation use cases are possible. However, it's important to exercise caution in languages where benchmarks aren't yet established. The team values transparency, and potential users should be aware of this caveat.

Therefore, any outputs or usage of Writer LLM should always include guidance from a human expert. The team continuously evaluates and refines capabilities, and learning happens with customers.

| Language            | MMLU/MLMM | BLEU (source \ English) |
| :------------------ | :-------- | :---------------------- |
| Arabic              | 68.9      | 61.2                    |
| Bengali             | 63.3      | 54.4                    |
| Bulgarian           | 76.3      | 64.2                    |
| Chinese simplified  | 71.7      | 63.8                    |
| Chinese traditional | 73.7      | 57.0                    |
| Croatian            | 64.9      | 66.4                    |
| Czech               | -         | 52.5                    |
| Danish              | 77.7      | 70.5                    |
| Dutch               | 73.6      | 73.9                    |
| English             | 70.2      | -                       |
| Finnish             | -         | 68.9                    |
| French              | 69.1      | 63.1                    |
| German              | 70.4      | 71.3                    |
| Greek               | -         | 60.4                    |
| Hebrew              | -         | 67.8                    |
| Hindi               | 77.9      | 68.4                    |
| Hungarian           | 67.7      | 65.3                    |
| Indonesian          | 67.8      | 63.5                    |
| Italian             | 72.5      | 70.9                    |
| Japanese            | 73.5      | 66.8                    |
| Korean              | -         | 56.8                    |
| Lithuanian          | -         | 59.3                    |
| Polish              | -         | 60.6                    |
| Portuguese          | -         | 66.2                    |
| Romanian            | 70.9      | 67.6                    |
| Russian             | 75.1      | 65.2                    |
| Spanish             | 72.5      | 79.3                    |
| Swahili             | -         | 62.8                    |
| Swedish             | -         | 63.2                    |
| Thai                | -         | 54.7                    |
| Turkish             | 64.1      | 57.5                    |
| Ukrainian           | 75.2      | 68.0                    |
| Vietnamese          | 72.5      | 60.3                    |

# Dialect support

Writer Palmyra LLM also supports outputting in specific language dialects. The best results come from using a prompt with the following characteristics:

1. The prompt itself is in the desired language and dialect
2. The prompt clearly describes the type of dialect, for example, "It's essential that you use the Spanish spoken in Spain."
3. The prompt provides specific examples of the dialect, both vocabulary and grammatical differences

The following example, although not in the desired language for simplicity's sake, is an example of an optimal prompt that asks for a translation in Spanish spoken in Spain.

```text theme={null}
Hello, good afternoon! I need you to help me translate the following text. It's essential that you use the Spanish spoken in Spain. For example, you should use words like "coche" and/or "patata" instead of "carro" and/or "pap." Additionally, you need to pay attention to grammatical differences, such as the use of "voy a por" (Spain) instead of "voy por" (Latin America), or the structure of sentences like "hoy he comido una manzana" instead of "hoy comí una manzana." I prefer that you use "vosotros" (speak) instead of "ustedes" (speak), unless it's necessary to write very formally. 
```

> Here is the text to translate:\
> \[text you want translated]

```

## Basic prompt examples

### Translation

> Read the content of this source. Provide me with a translation of all its contents in French: https://writer.com/blog/ai-guardrails/

### Text generation

> Please write a blog post about the importance of productivity for small businesses in Arabic.

### Native multi-language support

> 人工知能の歴史と大規模言語モデルの開発について、短い段落を書いてください。読者はビジネステクノロジーニュースに興味がありますが、技術的なバックグラウンドはありません。技術的な概念を8年生の読解レベルで簡潔に説明してください。
```


# Configure connectors
Source: https://dev.writer.com/home/mcp-gateway

Configure prebuilt and custom connectors for WRITER Agent to securely access enterprise tools and your own APIs

This page covers technical details about configuring connectors for your organization, including authentication setup, access control, and security considerations. Connectors enable WRITER Agent to access enterprise tools and custom APIs through Writer's Model Context Protocol (MCP) gateway.

Writer provides two types of connectors:

* **Prebuilt connectors**: Ready-to-use integrations with popular enterprise tools like Slack and Google Workspace
* **Custom connectors**: Connectors you create from OpenAPI specifications or MCP servers to integrate with internal tools and third-party services

After you configure connectors in AI Studio, users in your organization can connect to them through [WRITER Agent](https://app.writer.com/). For step-by-step connector configuration instructions, see [Setting up connectors](https://support.writer.com/article/299-setting-up-connectors) in the Writer Help Center.

<Warning>
  MCP connectors are currently available for [WRITER
  Agent](https://app.writer.com/). Connectors do not currently support Agent
  Builder, no-code agents, or API integrations.
</Warning>

## How MCP gateway works

Writer's MCP gateway sits between WRITER Agent and connector servers (both prebuilt and custom). This architecture provides centralized security, access control, and observability for enterprise deployments. All connector requests—whether prebuilt or custom—flow through the MCP gateway.

Connector requests flow through the following steps:

1. **Client connection**: WRITER Agent connects to Writer's unified MCP server
2. **Request submission**: WRITER Agent sends HTTP requests to the MCP gateway endpoint
3. **Authentication**: The gateway validates user identity and permissions using your Writer organization's access controls
4. **Request routing**: Authenticated requests are routed to the appropriate partner MCP server
5. **Response delivery**: Results are returned to the agent through the secure channel

```mermaid theme={null}
sequenceDiagram
    participant Agent as WRITER Agent
    participant Gateway as MCP Gateway<br/>(Writer)
    participant Auth as Access Control<br/>(Organization/Team/User)
    participant MCP as Partner MCP Server<br/>(Slack, Google, etc.)

    Agent->>Gateway: Request tools/execute action
    Gateway->>Auth: Validate user identity & permissions
    Auth-->>Gateway: User authorized ✓
    Gateway->>MCP: Forward authenticated request
    MCP-->>Gateway: Return results
    Gateway-->>Agent: Deliver response
```

## Available connectors

Writer provides two types of connectors: prebuilt connectors for popular enterprise tools and custom connectors you create from OpenAPI specifications or MCP servers.

### Prebuilt connectors

Prebuilt connectors are ready-to-use integrations maintained by Writer. These connectors provide access to popular enterprise tools with pre-configured authentication and tool definitions.

| Connector                    | Auth type | Managed by                     | Connector docs                                    |
| ---------------------------- | --------- | ------------------------------ | ------------------------------------------------- |
| **Adobe Experience Manager** | OAuth 2.0 | Adobe-managed                  | [View docs](/connectors/adobe-experience-manager) |
| **Asana**                    | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/asana)                    |
| **Atlassian**                | OAuth 2.0 | Atlassian-managed              | [View docs](/connectors/atlassian)                |
| **Contentful**               | OAuth 2.0 | Contentful-managed             | [View docs](/connectors/contentful)               |
| **Databricks**               | API key   | Organization-managed           | [View docs](/connectors/databricks)               |
| **FactSet**                  | OAuth 2.0 | Organization-managed           | [View docs](/connectors/factset)                  |
| **Gmail**                    | OAuth 2.0 | Organization-managed           | [View docs](/connectors/gmail)                    |
| **Gong**                     | API key   | Organization-managed           | [View docs](/connectors/gong)                     |
| **Google BigQuery**          | OAuth 2.0 | Organization-managed           | [View docs](/connectors/google-bigquery)          |
| **Google Calendar**          | OAuth 2.0 | Organization-managed           | [View docs](/connectors/google-calendar)          |
| **Google Docs**              | OAuth 2.0 | Organization-managed           | [View docs](/connectors/google-docs)              |
| **Google Drive**             | OAuth 2.0 | Organization-managed           | [View docs](/connectors/google-drive)             |
| **Google Sheets**            | OAuth 2.0 | Organization-managed           | [View docs](/connectors/google-sheets)            |
| **HubSpot**                  | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/hubspot)                  |
| **Microsoft Calendar**       | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/microsoft-calendar)       |
| **Microsoft OneDrive**       | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/microsoft-onedrive)       |
| **Microsoft Outlook**        | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/microsoft-outlook)        |
| **Microsoft SharePoint**     | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/microsoft-sharepoint)     |
| **Microsoft Teams**          | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/microsoft-teams)          |
| **PitchBook**                | API key   | Organization-managed           | [View docs](/connectors/pitchbook)                |
| **Salesforce**               | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/salesforce)               |
| **Semrush**                  | OAuth 2.0 | Semrush-managed                | [View docs](/connectors/semrush)                  |
| **Slack**                    | OAuth 2.0 | Writer or organization-managed | [View docs](/connectors/slack)                    |
| **Snowflake**                | OAuth 2.0 | Organization-managed           | [View docs](/connectors/snowflake)                |
| **Wrike**                    | OAuth 2.0 | Organization-managed           | [View docs](/connectors/wrike)                    |
| **ZoomInfo**                 | OAuth 2.0 | Organization-managed           | [View docs](/connectors/zoominfo)                 |

### Custom connectors

Custom connectors let you integrate WRITER Agent with any API that has an OpenAPI specification or any external MCP server. Use custom connectors to connect to internal tools, proprietary systems, or third-party services not available as prebuilt connectors.

Once created, custom connectors are configured and managed in AI Studio the same way as prebuilt connectors. You control authentication, access permissions, and which tools are enabled for users and teams.

For step-by-step instructions on creating custom connectors, see [Custom connectors](/home/custom-connectors).

## Set up connectors

Connector setup happens in [AI Studio](https://app.writer.com/aistudio) under **Connectors & Tools**. The setup process differs slightly between connector types:

* **Prebuilt connectors**: Select from the connector library and configure authentication. For step-by-step instructions, see [Setting up connectors](https://support.writer.com/article/299-setting-up-connectors).
* **Custom connectors**: After creating a custom connector, configure access permissions, enable tools, and enter authentication credentials. For instructions on creating custom connectors, see [Custom connectors](/home/custom-connectors).

The sections below cover the technical authentication requirements for configuring connectors.

## Set up connector authentication

Both prebuilt and custom connectors support OAuth 2.0 and API key authentication. The type of authentication depends on the third-party service, and the connector configuration flow in AI Studio guides you through the required setup.

**OAuth 2.0 connectors**:

* Most prebuilt OAuth connectors support WRITER-managed authentication (no OAuth app setup required)
* Custom connectors and some prebuilt connectors (FactSet, Gmail, Google BigQuery, Google Calendar, Google Docs, Google Drive, Google Sheets, Snowflake, Wrike, ZoomInfo) require organization-managed OAuth
* The Adobe Experience Manager, Atlassian, Contentful, and Semrush connectors use dynamic client registration, where the provider automatically creates and manages OAuth credentials (no manual OAuth app setup required)
* See [Set up OAuth](#set-up-oauth) for details on all options

**API key connectors**:

* Require an API key from the third-party service
* For prebuilt connectors, see the [prebuilt connectors](#prebuilt-connectors) table for links to provider documentation
* For custom connectors, see [Custom connectors](/home/custom-connectors) for setup instructions

### Set up OAuth

OAuth connectors support three possible authentication options in AI Studio:

**WRITER-managed OAuth** (prebuilt connectors only):

* Writer provides and manages the OAuth application
* No OAuth app setup required
* Users authorize access to their account when they first use the connector
* Faster setup with less configuration

**Organization-managed OAuth** (prebuilt and custom connectors):

* You create and manage your own OAuth application with the provider
* Provides more control and custom branding
* Requires OAuth app configuration with client ID and client secret
* Required for custom connectors and the following prebuilt connectors: FactSet, Gmail, Google BigQuery, Google Calendar, Google Docs, Google Drive, Google Sheets, Snowflake, Wrike, ZoomInfo

**Dynamic client registration** ([Adobe Experience Manager](/connectors/adobe-experience-manager), [Atlassian](/connectors/atlassian), [Contentful](/connectors/contentful), and [Semrush](/connectors/semrush)):

* The provider automatically creates and manages OAuth credentials during the setup process
* No manual OAuth application setup, client ID, or client secret required
* Faster setup with less configuration

#### OAuth connection levels

When configuring an OAuth connector, you choose whether to use user-level or org-level authentication:

**User-level OAuth** (most common):

* Each user connects their own personal account (their Gmail, Slack, etc.)
* OAuth authentication happens when individual users first try to use the connector
* Administrators configure the connector in AI Studio, but don't authenticate during setup
* Users authenticate through WRITER Agent the first time they interact with the connector

**Org-level OAuth**:

* A single shared OAuth connection for the entire organization
* OAuth authentication happens during connector setup in AI Studio
* Administrators authenticate with the provider when configuring the connector
* Users can immediately use the connector without individual authentication

<Note>
  With user-level OAuth, administrators set up the connector configuration
  (OAuth app credentials, enabled tools, access permissions), but the actual
  OAuth authentication happens later when each user first uses the connector.
  With org-level OAuth, administrators complete the OAuth authentication during
  the initial connector setup.
</Note>

#### Create OAuth applications for organization-managed authentication

If you choose organization-managed OAuth, or if using a connector that requires it, you need to create an OAuth application with the provider:

1. Follow the provider's documentation to create an OAuth application. View [prebuilt connectors](#prebuilt-connectors) for links to provider documentation.

2. Add the following Writer redirect URI to the list of allowed redirect URIs in your OAuth application's settings:

   ```
   https://app.writer.com/mcp/oauth/callback
   ```

   <Note>
     If your organization is deployed in the EU, use `https://app.eu2.writer.com/mcp/oauth/callback` instead.
   </Note>

3. Copy the client ID and client secret for use in AI Studio

The gateway uses this redirect URI to receive authorization codes from third-party services after users grant permission.

#### Required OAuth scopes

Each connector requires specific OAuth scopes to access third-party APIs. The connector configuration interface in AI Studio displays the required scopes when you configure a connector.

<img alt="Google Calendar OAuth scopes" />

Some connectors require extensive OAuth scopes during authentication. This is because connectors often provide multiple tools with broad functionality across the integrated service.

For example, a connector for a productivity service might include tools to:

* Read and search documents
* Create and modify files
* Access calendar events
* Send messages or notifications
* Manage sharing permissions

Each of these capabilities requires corresponding OAuth scopes from the provider.

Connectors use either dynamic or fixed OAuth scopes. For details on how scopes work and which connectors support dynamic scopes, see [OAuth scopes](#oauth-scopes).

## Manage access control and permissions

Organization and IT administrators can control which users have access to connectors and configure granular permissions for connector functionality.

### Configure connector availability

Connectors can be made available to users in two ways:

* **All users**: The connector is immediately available to everyone in the organization
* **Select teams**: Restrict the connector to specific teams. Users on non-approved teams can request access, which requires admin approval.

This access control model allows organizations to gradually roll out connectors or restrict sensitive integrations to specific teams.

### Role-based administration

By default, only users with Org Admin and IT Admin roles can configure and manage connectors. These permissions can be adjusted through the roles and permissions settings in AI Studio to delegate connector management to other roles if needed.

### Tool-level granular control

Within each connector, administrators can selectively enable specific tools rather than activating all available functionality. This provides fine-grained control over what actions AI agents can perform through each connector.

For example, you might enable read-only tools for a connector while disabling tools that modify or delete data.

### Revoke connector access

Administrators and users can revoke connector access at multiple levels:

**Revoke individual user access:**

* Users can disconnect their personal OAuth accounts from within WRITER Agent settings
* Disconnecting removes the user's authentication tokens but preserves the connector configuration
* Users can reconnect by re-authorizing through the OAuth flow

**Revoke team or organization access:**

* Administrators can disable a connector entirely from AI Studio's Connectors & Tools interface
* Disabling a connector immediately revokes access for all users
* Re-enabling requires users to re-authenticate with their accounts

**Revoke specific tool permissions:**

* Administrators can disable individual tools within a connector without removing the entire connector
* Users retain their OAuth connections but cannot access disabled tools
* Tool permissions can be re-enabled without requiring re-authentication

## Private endpoints

A private endpoint is a network path between Writer's MCP gateway and a service in your VPC. Instead of sending connector traffic over the public internet, Writer connects through [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/privatelink/what-is-privatelink.html) or [GCP Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect) so requests stay on your cloud provider's private network.

Register private endpoints in AI Studio under **Connectors & Tools > Private endpoints**. After you register an endpoint, attach it when you configure a connector so WRITER Agent uses that path for that connector's requests. For cloud provider setup, registration, and how to enable an endpoint on a connector, see [Private endpoints](/home/private-endpoints).

<Note>
  For prebuilt connectors, contact your customer success manager to enable
  private endpoints. You can enable private endpoints on custom connectors
  during connector setup in AI Studio.
</Note>

## Whitelist IP addresses

If your organization’s firewall or network policies restrict connections from third-party services, add Writer’s MCP Gateway static egress IP addresses to your allowlist.

All outbound traffic from prebuilt and custom connectors originates from the following static IP addresses:

<Expandable title="static egress IP addresses">
  | IP address        |
  | ----------------- |
  | `136.114.152.14`  |
  | `136.114.183.118` |
  | `136.114.77.197`  |
  | `136.116.231.122` |
  | `34.10.120.41`    |
  | `34.132.102.189`  |
  | `34.132.239.48`   |
  | `34.136.80.118`   |
  | `34.136.9.195`    |
  | `34.30.77.246`    |
  | `34.41.53.121`    |
  | `34.56.164.176`   |
  | `34.58.72.208`    |
  | `34.63.201.5`     |
  | `34.68.86.48`     |
  | `35.225.95.201`   |
</Expandable>

<Warning>
  IP addresses are subject to change. Last updated **March 10, 2026**.
</Warning>

## Connect as an end user

After administrators configure connectors in AI Studio, end users interact with them through conversation in WRITER Agent.

### User-level and org-level OAuth

Connectors can be configured with different OAuth authentication levels, which affects how users connect:

**User-level OAuth** (most common):

* Each user connects their own personal account (their Gmail, their Slack workspace, etc.)
* Users authenticate individually with their own credentials
* The connector accesses only that user's data and permissions
* Example: When you use the Gmail connector, it accesses your Gmail inbox, not a shared organizational inbox

**Org-level OAuth** (shared connection):

* An administrator sets up a single OAuth connection for the entire organization
* All users share the same authenticated connection
* No individual user authentication required
* Less common; typically used when accessing shared organizational resources rather than personal accounts

<Note>
  Most connectors use user-level OAuth where each person connects their own
  account. When a connector is configured with user-level OAuth, users must
  authenticate individually before using it.
</Note>

### How users interact with connectors

To use a connector, ask WRITER Agent for what you need in everyday language, such as "Summarize unread emails" or "Find messages in Slack." If access is needed, the agent prompts you to connect your account. After you're connected, you can keep making requests the same way—no special steps or commands required.

**Example interactions:**

**Using Gmail:**

```
User: "Summarize my unread emails from this week"
Agent: [If not connected] "I need access to your Gmail to help with that.
       Please connect your Gmail account to continue."
       [Shows authentication button]

User: [Clicks to authenticate, completes OAuth flow]

Agent: "Thanks! I can now access your Gmail. Here's a summary of your
       unread emails from this week..."
```

### First-time connection flow

For connectors configured with **user-level OAuth**, users must authenticate their personal account the first time they use a connector:

1. **User makes a request**: "Search Slack for messages about the budget"
2. **Authentication prompt**: Agent responds: "I need access to your Slack workspace to search messages. Please connect your Slack account."
3. **OAuth authorization**: User clicks the authentication button and is redirected to Slack's authorization page
4. **Permission consent**: User reviews the permissions and approves access
5. **Return to conversation**: User is redirected back to WRITER Agent
6. **Task completion**: Agent automatically retries the original request: "I found 15 messages about the budget..."

For connectors configured with **org-level OAuth**, users can immediately use the connector without individual authentication since the administrator has already set up the shared connection.

## Known limitations

### OAuth token expiration

Some providers automatically expire OAuth tokens after periods of inactivity:

* **Google services**: OAuth credentials become invalid after 6 months of inactivity. If you haven't used a Google connector (Gmail, Google BigQuery, Google Calendar, Google Docs, Google Drive, Google Sheets) for 6 months, you'll need to re-authenticate.

The MCP gateway automatically handles token refresh for active connections, but cannot refresh tokens that have been revoked due to inactivity.

### OAuth scopes

OAuth scopes define the permissions a connector receives during authorization. Depending on the connector, the OAuth application requests scopes as either a fixed set or a dynamic set based on the tools you select during configuration.

#### Fixed scopes

With fixed-scope OAuth connectors, the OAuth application requests all scopes required for every available tool when you authorize the connector, regardless of which tools you enable or disable in your configuration.

#### Dynamic scopes

With dynamic-scope OAuth connectors, the OAuth application requests only the scopes needed for the tools you enable.

The following connectors support dynamic OAuth scopes:

* **Google**: [Gmail](/connectors/gmail), [Google BigQuery](/connectors/google-bigquery), [Google Calendar](/connectors/google-calendar), [Google Docs](/connectors/google-docs), [Google Drive](/connectors/google-drive), [Google Sheets](/connectors/google-sheets)
* **Microsoft**: [Microsoft Calendar](/connectors/microsoft-calendar), [Microsoft OneDrive](/connectors/microsoft-onedrive), [Microsoft Outlook](/connectors/microsoft-outlook), [Microsoft SharePoint](/connectors/microsoft-sharepoint), [Microsoft Teams](/connectors/microsoft-teams)
* **Gong**: [Gong](/connectors/gong)

## Next steps

* [Custom connectors](/home/custom-connectors): Create connectors from OpenAPI specifications or MCP servers to integrate with your own APIs and tools.
* [Private endpoints](/home/private-endpoints): Connect over a private network using AWS PrivateLink or GCP Private Service Connect.
* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Learn how to configure and enable prebuilt connectors in AI Studio.
* [Tool calling guide](/home/tool-calling): Understand how AI agents use tools in conversations.
* [WRITER Agent guide](https://support.writer.com/article/293-how-to-use-action-agent): Learn how to use WRITER Agent with connected tools.
* [API reference](/api-reference/completion-api/chat-completion): Explore the Chat Completion API that powers tool calling.


# MCP server
Source: https://dev.writer.com/home/mcp-server



This guide shows you how to use the Writer MCP server to orchestrate multiple Writer API calls through AI assistants like [Claude Desktop](https://claude.ai/download) and [Cursor](https://www.cursor.com/), or your own custom MCP client.

The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI assistants to interact with external tools and data sources. After completing these steps, you can chain together multi-step workflows without hardcoding API calls.

## Install and configure the MCP server

### Set up your Writer API key

The MCP server requires your Writer API key as an environment variable. If you don't have an API key, you can [create one](/api-reference/api-keys).

Store your API key in a `.env` file with the name `WRITER_API_KEY`. For example:

```bash theme={null}
export WRITER_API_KEY=your-api-key-here
```

Then source the environment file in your shell:

```bash theme={null}
source .env
```

<Note>
  The MCP server will show a fatal error if the `WRITER_API_KEY` environment variable is missing or empty. Make sure to set this environment variable before running the MCP server.
</Note>

### Install and run the MCP server

You can install and run the Writer MCP server using [`npx`](https://docs.npmjs.com/cli/v8/commands/npx). Set the `WRITER_API_KEY` environment variable before running the MCP server.

The following command installs and runs the latest version of the Writer MCP server:

```bash theme={null}
npx -y writer-sdk-mcp@latest
```

<Note>
  You can also install the MCP server globally and run it anytime.

  ```bash theme={null}
  npm install -g writer-sdk-mcp

  mcp-server
  ```
</Note>

### Configure your MCP client

Add the Writer MCP server to your MCP client configuration, such as [`claude_desktop_config.json` for Claude Desktop](https://modelcontextprotocol.io/docs/develop/connect-local-servers) or [`mcp.json` for Cursor](https://cursor.com/docs/context/mcp). For example, here's how to configure the MCP server for Claude Desktop:

<Tabs>
  <Tab title="Claude Desktop">
    Add to your [`claude_desktop_config.json`](https://modelcontextprotocol.io/docs/develop/connect-local-servers):

    ```json claude_desktop_config.json theme={null}
    {
      "mcpServers": {
        "writer": {
          "command": "npx",
          "args": ["-y", "writer-sdk-mcp@latest"],
          "env": {
            "WRITER_API_KEY": "your-api-key-here"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="Cursor">
    Add to your [`mcp.json`](https://cursor.com/docs/context/mcp):

    ```json mcp.json theme={null}
    {
      "mcpServers": {
        "writer": {
          "command": "npx",
          "args": ["-y", "writer-sdk-mcp@latest"],
          "env": {
            "WRITER_API_KEY": "your-api-key-here"
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

### Interact with the MCP server in an AI client

After you've added your MCP server to your client configuration, you can interact with it by sending requests to the MCP server. Each client has a different way to interact with the MCP server.

For conversational AI assistants like Claude Desktop and Cursor, you can interact with the MCP server by sending messages to the assistant. For example, to upload a file to Writer, you can send a message like "Upload this image to Writer" and the assistant can use the MCP server to accomplish the task.

## Build custom MCP clients

You can also create your own MCP client using any MCP-compatible package like [Strands](https://strandsagents.com/latest/), [LangChain](https://python.langchain.com/), or other MCP libraries. The Writer MCP server follows the standard MCP protocol, so you can integrate it with any client that implements the protocol.

The following example shows how to create a custom MCP client using the Strands SDK.

### Set up the Strands SDK

First, install the Strands SDK, the Writer dependency, and the Strands Agent Tools package:

```bash theme={null}
pip install 'strands-agents[writer]' strands-agents-tools
```

### Create a custom MCP client

Next, create a custom MCP client that uses the Writer MCP server to respond to user requests.

The example first creates an `MCPClient` using Strands Tools to connect to the Writer MCP server, then sets up the Writer model to be used by the agent for handling requests.

The `writer_mcp_client.list_tools_sync()` method retrieves the list of available tools from the Writer MCP server. The agent then uses these tools to respond to user requests related to content workflow automation.

```python content_workflow_client.py theme={null}
import os
from strands import Agent
from strands.models.writer import WriterModel
from strands.tools.mcp import MCPClient
from mcp import stdio_client, StdioServerParameters

# Connect to the Writer MCP server using stdio transport
writer_mcp_client = MCPClient(lambda: stdio_client(
    StdioServerParameters(
        command="npx",
        args=["-y", "writer-sdk-mcp@latest"],
        env={"WRITER_API_KEY": os.environ["WRITER_API_KEY"]}
    )
))

# Initialize the Writer model
writer_model = WriterModel(
    client_args={"api_key": os.environ["WRITER_API_KEY"]},
    model_id="palmyra-x5",
)

def content_workflow_client(user_request: str):
    """Custom MCP client that orchestrates complex content workflows using Writer MCP tools."""
    with writer_mcp_client:
        tools = writer_mcp_client.list_tools_sync()
        agent = Agent(
            model=writer_model,
            tools=tools,
            system_prompt="""You are a content workflow assistant that can:
            - Upload and analyze files
            - Generate content using Writer's models
            - Query Knowledge Graphs for research
            - Analyze images and extract insights
            - Chain multiple operations together seamlessly
            
            Use the available Writer MCP tools to fulfill user requests efficiently."""
        )
        return agent(user_request)

# Example usage
if __name__ == "__main__":
    # Research and content generation task
    response = content_workflow_client("Research and create content about sustainable fashion trends, including a blog post outline and social media content ideas")
    print("Research & Content:", response)
```

### Troubleshoot common issues

**MCP server shows fatal error about missing API key**: verify your API key is set correctly:

```bash theme={null}
echo $WRITER_API_KEY
```

If the environment variable is not set, run:

```bash theme={null}
export WRITER_API_KEY=your-api-key-here
```

**MCP client fails to initialize with "Connection closed" error**: this usually means the MCP server couldn't start due to a missing API key. When using Strands SDK, make sure to pass the environment variable to the MCP server:

```python theme={null}
# Correct way to pass environment variables to MCP server
writer_mcp_client = MCPClient(lambda: stdio_client(
    StdioServerParameters(
        command="npx",
        args=["-y", "writer-sdk-mcp@latest"],
        env={"WRITER_API_KEY": os.environ["WRITER_API_KEY"]}  # This is required!
    )
))
```

**Tools not appearing**: check that the MCP server is properly configured in your client and restart the client application.

**Permission errors**: ensure your API key has the necessary permissions for the operations you want to perform.

## Next steps

Now that you've set up the MCP server, you can use it to build complex workflows without writing custom integration code. Next, you can explore the following resources to help you get the most out of the MCP server:

* Explore [Writer's API reference](/api-reference) to understand all available endpoints
* Learn about [tool calling](/home/tool-calling) for more advanced integrations
* Check out [Knowledge Graphs](/home/knowledge-graph) for building intelligent content systems
* Review [chat with images](/home/chat-with-images) for image processing workflows


# Mitigate bias in AI outputs
Source: https://dev.writer.com/home/mitigating_bias



## Introduction

In the era of artificial intelligence, ensuring that AI models produce unbiased and fair outputs has become a critical concern. Writer, one of the leading organizations in the field, takes this challenge seriously and employs a range of strategies to detect and mitigate biases in its AI models. In this article, we explore the technical methodologies employed by Writer to grapple with this complex issue.

## Mechanisms and methodologies for detecting and mitigating Bias

<Tabs>
  <Tab title="Data cleaning and preprocessing">
    At the heart of any machine learning model lies the data on which it's trained. Writer meticulously curates its training data, which is sourced from a mix of publicly available data from the Internet and data licensed from third parties. Text preprocessing techniques, such as removing sensitive content or flagging potential hotspots for bias, are employed before the data is used for training.
  </Tab>

  <Tab title="Annotation guidelines">
    The first layer of oversight comes in the form of human annotators who label the data. These annotators are guided by a stringent set of rules designed to counteract the introduction of bias. For example, they're trained to avoid favoring any political ideology, ethnicity, or other sociocultural factors when annotating the training data.
  </Tab>

  <Tab title="Auditing">
    After the initial training, the model is subjected to a rigorous auditing process. This involves running the model through a series of test cases designed to gauge its propensity for biased or unsafe content. These audits are often carried out by both internal teams and third-party organizations to ensure objectivity.
  </Tab>

  <Tab title="RLHF">
    The next step involves fine-tuning the model based on the results of the audit. Writer uses reinforcement learning from human feedback (RLHF) or similar techniques to adjust the model's parameters, helping it to make less biased decisions. Fine-tuning focuses on specific aspects like language nuances, sentiment interpretation, and context-aware decision-making.
  </Tab>

  <Tab title="Feedback loop">
    Writer has an active feedback loop with its beta user community. Reports of biased outputs are taken seriously and are used to further fine-tune the model. This makes the model more robust and adaptable to real-world applications.
  </Tab>
</Tabs>

## Sources of bias in training data

Writer models are trained on large datasets that are a snapshot of human culture and thought, collected by our team. While this helps the model to be versatile and knowledgeable, it also brings in the risk of the model inheriting the existing biases in society. Writer mitigates this by adding layers of scrutiny and control, both algorithmic and human, on the data used for training.

## Adapting to different contexts and languages

<Tabs>
  <Tab title="Context-aware fine-tuning">
    Writer is pioneering research in making its models more context-sensitive. This involves incorporating additional features into the model’s architecture that allows it to understand the specific context in which a text snippet appears, enabling more nuanced responses.
  </Tab>

  <Tab title="Language-specific models">
    Given the global reach of AI, Writer is also working on language-specific versions of its models. These models undergo fine-tuning with data that is representative of the linguistic and cultural idiosyncrasies of specific languages.
  </Tab>

  <Tab title="Annotator teams">
    To further the goal of international adaptability, Writer often employs annotators from various cultural backgrounds. This helps in creating a model that is less likely to favor any particular group.
  </Tab>
</Tabs>

## Conclusion

The challenge of eliminating bias in AI models is a complex and ongoing task. Writer employs a multi-faceted approach, combining data science, human oversight, and cutting-edge machine learning techniques, to tackle this critical issue. While there's always room for improvement, the methodologies adopted serve as a strong framework for mitigating bias in AI.


# Use another LLM as a tool
Source: https://dev.writer.com/home/model-delegation



With model delegation, you can use another model as a tool in a [chat completion](/home/chat-completion). The predefined LLM tool allows you to delegate specific tasks to another model available to your organization, including [external models](/home/external-models) from providers like AWS Bedrock and Microsoft Azure.

For example, in a chat application using Palmyra X5, you can delegate focused analysis tasks to a different model.

This guide shows you how to set up model delegation with the Writer API. After completing these steps, you can route specific tasks within a chat completion to the model best suited to handle them.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Tool structure

Use the LLM tool to delegate specific tasks to another model when using the [chat endpoint](/api-reference/completion-api/chat-completion). Using [tool calling](/home/tool-calling), you can specify the model you want to use for a given task. When the primary chat model calls the LLM tool based on the user's input, it signals it in the chat API response.

To use the LLM tool, add it to the `tools` array in your `chat-completion` endpoint request.

The LLM tool object has the following structure:

| Parameter              | Type     | Description                                                                                                                                                               |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                 | `string` | The type of tool, which is `llm` for LLM tool                                                                                                                             |
| `function`             | `object` | An object containing the tool's description and model                                                                                                                     |
| `function.description` | `string` | A description of what the model will be used for.                                                                                                                         |
| `function.model`       | `string` | The ID of the model to be used for this tool. This can be a [Palmyra model](/home/models) or an [external model](/home/external-models) configured for your organization. |

<Note>
  To help the model understand when to use the tool, follow these best practices for the `function.description` parameter:

  * Indicate that the tool is a function that invokes an LLM
  * Specify the model's purpose and capabilities
  * Describe when the tool should be used

  An example description for a tool using an external model:

  > "A function that invokes the LLM identified by the given model for detailed analysis. Any user request requiring in-depth analysis should use this tool."
</Note>

<CodeGroup>
  ```bash cURL theme={null}
  "tools": [
      {
          "type": "llm",
          "function": {
              "description": "A function that will invoke the llm identified by the given model for detailed analysis and reporting. Any user request requiring in-depth analysis should use this tool.",
              "model": "anthropic.claude-3-sonnet-20240229-v1:0"
          }
      }  
  ]
  ```

  ```python Python theme={null}
  tools = [{
      "type": "llm",
      "function": {
          "description": "A function that will invoke the llm identified by the given model for detailed analysis and reporting. Any user request requiring in-depth analysis should use this tool.",
          "model": "anthropic.claude-3-sonnet-20240229-v1:0"
      }
  }]
  ```

  ```js JavaScript theme={null}
  const tools = [{
      type: "llm",
      function: {
          description: "A function that will invoke the llm identified by the given model for detailed analysis and reporting. Any user request requiring in-depth analysis should use this tool.",
          model: "anthropic.claude-3-sonnet-20240229-v1:0"
      }
  }]
  ```
</CodeGroup>

<Note>
  You can only pass one prebuilt tool in the `tools` array at a time. However, you can pass multiple [custom tools](/home/tool-calling) in the same request.

  Prebuilt tools are:

  * LLM tool
  * [Knowledge Graph tool](/home/kg-chat)
  * [Translation tool](/home/translation-tool)
  * [Web search tool](/home/web-search-tool)
</Note>

### Response format

When a chat completion uses the LLM tool, the response from the LLM tool is in the `llm_data` object. The `llm_data` object contains the following fields:

| Parameter | Type   | Description                               |
| --------- | ------ | ----------------------------------------- |
| `prompt`  | string | The prompt used by the LLM tool.          |
| `model`   | string | The ID of the model used by the LLM tool. |

Below is an example of the full response to a chat completion request that uses the LLM tool with an external model.

<CodeGroup>
  ```json non-streaming response [expandable] {17} theme={null}
  {
    "id": "1234",
    "object": "chat.completion",
    "choices": [
      {
        "index": 0,
        "finish_reason": "stop",
        "message": {
          "content": "The recommended daily intake of calcium for a 30-year-old woman is 1,000 mg per day.",
          "role": "assistant",
          "tool_calls": null,
          "graph_data": {
            "sources": null,
            "status": null,
            "subqueries": null
          },
          "llm_data": {
            "prompt": "What is the recommended daily intake of calcium for a 30-year-old woman?",
            "model": "anthropic.claude-3-sonnet-20240229-v1:0"
          },
          "image_data": null,
          "refusal": null
        },
        "logprobs": null
      }
    ],
    "created": 1741970653,
    "model": "palmyra-x5",
    "usage": {
      "prompt_tokens": 259,
      "total_tokens": 305,
      "completion_tokens": 46,
      "prompt_token_details": null,
      "completion_tokens_details": null
    },
    "system_fingerprint": "v1",
    "service_tier": null
  }
  ```

  ```json streaming response [expandable] {16} theme={null}
  {
      "id": "1234",
      "object": "chat.completion.chunk",
      "choices": [{
          "index": 0,
          "finish_reason": None,
          "message": {
              "content": "The",
              "role": "assistant",
              "tool_calls": None,
              "graph_data": {
                  "sources": None,
                  "status": None,
                  "subqueries": None
              },
              "llm_data": {
                  "prompt": "What is the recommended daily intake of calcium for a 30-year-old woman?",
                  "model": "anthropic.claude-3-sonnet-20240229-v1:0"
              },
              "image_data": None,
              "refusal": None
          },
          "logprobs": None,
      "delta": {
          "content": "The",
          "role": "assistant",
          "tool_calls": None,
          "graph_data": {
              "sources": None,
              "status": None,
              "subqueries": None
          },
          "llm_data": {
              "prompt": "What is the recommended daily intake of calcium for a 30-year-old woman?",
              "model": "anthropic.claude-3-sonnet-20240229-v1:0"
          },
          "image_data": None,
          "refusal": None
      },
      "logprobs": None
      }],
      "created": 1741970696,
      "model": "palmyra-x5",
      "usage": None,
      "system_fingerprint": "v1",
      "service_tier": None
  }
  ```
</CodeGroup>

## Usage example

Here's an example of how to use the LLM tool in your application. This example delegates detailed questions to an external model.

### Create a tools array containing an LLM tool

To use the LLM tool, create a `tools` array that specifies the model you want to use.

<CodeGroup>
  ```bash cURL theme={null}
  "tools": [
      {
          "type": "llm",
          "function": {
              "description": "A function that will invoke the llm identified by the given model for detailed analysis. Any user request requiring in-depth analysis should use this tool.",
              "model": "anthropic.claude-3-sonnet-20240229-v1:0"
          }
      }
  ]
  ```

  ```python Python theme={null}
  tools = [{
      "type": "llm",
      "function": {
          "description": "A function that will invoke the llm identified by the given model for detailed analysis. Any user request requiring in-depth analysis should use this tool.",
          "model": "anthropic.claude-3-sonnet-20240229-v1:0"
      }
  }]
  ```

  ```js JavaScript theme={null}
  const tools = [{
      type: "llm",
      function: {
          description: "A function that will invoke the llm identified by the given model for detailed analysis. Any user request requiring in-depth analysis should use this tool.",
          model: "anthropic.claude-3-sonnet-20240229-v1:0"
      }
  }]
  ```
</CodeGroup>

### Send the request using chat completions

Add the tools array to the chat endpoint call along with your array of messages. Setting `tool_choice` to `auto` allows the model to choose when to use the LLM tool, based on the user's question and the description of the tool.

This example streams the response as the model generates it.

If you are unfamiliar with the chat completions endpoint or streaming vs. non-streaming responses, learn more in the [chat completion guide](/home/chat-completion).

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "model": "palmyra-x5",
          "temperature": 0.7,
          "messages": [
              {
                  "role": "user",
                  "content": "What is the recommended daily intake of calcium for a 30-year-old woman?"
              }
          ],
          "tool_choice": "auto",
          "tools": [
              {
                  "type": "llm",
                  "function": {
                      "description": "A function that will invoke the llm identified by the given model for detailed analysis. Any user request requiring in-depth analysis should use this tool.",
                      "model": "anthropic.claude-3-sonnet-20240229-v1:0"
                  }
              }
          ],
          "stream": true
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  messages = [{"role": "user", "content": "What is the recommended daily intake of calcium for a 30-year-old woman?"}]

  response = client.chat.chat(
      model="palmyra-x5", 
      messages=messages, 
      tools=tools,  # The tools array defined earlier.
      tool_choice="auto",
      stream=True
  )

  for chunk in response:
      if chunk.choices[0].delta.content is not None:
          print(chunk.choices[0].delta.content, end="", flush=True)
  ```

  ```js JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const messages = [{role: "user", content: "What is the recommended daily intake of calcium for a 30-year-old woman?"}];

  const response = await client.chat.chat({
      model: "palmyra-x5", 
      messages: messages, 
      tools: tools, // The tools array defined earlier.
      tool_choice: "auto",
      stream: true
  });

  for await (const chunk of response) {
      if (chunk.choices[0].delta.content) {
          process.stdout.write(chunk.choices[0].delta.content);
      }
  }
  ```
</CodeGroup>

By following this guide, you can delegate specific tasks to another model within your chat applications.

## Next steps

Explore other ways to extend your chat applications:

* [Analyze images in a chat](/home/chat-with-images)
* [Pass questions to a Knowledge Graph](/home/kg-chat)
* [Search the web](/home/web-search-tool)
* [Translate text](/home/translation-tool)


# Configure model parameters
Source: https://dev.writer.com/home/model-parameters



## Likelihood

WRITER models learn language by reading text from the internet. Given a sentence such as "I like to play with Legos", the model repeatedly predicts what the next token is:

```text theme={null}

\*\*I [?]

I like [?]

I like to [?]

I like to play [?]

I like to play with [?]\*\*
```

The model learns that the word “to” is quite likely to follow the word “like” in English, and that the word “play” is likely to follow the word “with” and so on.

Likelihood refers to the probability that a given sequence of [tokens](/home/tokens), which can be words or characters, gets generated by the model. You can think of the likelihood of a token as a number that quantifies a model's level of surprise that this token appeared in a sentence.

If a token has a low likelihood, it means the model didn't expect this token to appear. Conversely, if a token has a high likelihood, the model was confident that it would appear.

You can use the likelihood score to evaluate the quality of the model's output, representing the model's confidence that a given output is the most likely sequence of tokens given the input prompt and the target data.

## Temperature

The same prompt may yield different outputs each time you hit "generate" and so, temperature is a parameter used to control the randomness and diversity of the generated output.

The temperature scale ranges from 0 to 1. A temperature of 1.0 means that the model is generating outputs with its normal level of confidence, based on the probabilities it has learned during training.

A higher temperature increases the randomness of the output, allowing for more diverse and unexpected results. A lower temperature makes the model more conservative in its predictions, generating outputs that are more likely to align with what it has learned during training.

Most people find that a temperature between 0.7 and 1 is a good starting point.

**Temperature defaults to 0.7** if not specified as an exact parameter

With longer prompts, the model becomes more confident in its predictions, so you can raise the temperature higher for a diverse and creative output without the output being too off topic. In contrast, using high temperatures on short prompts can lead to unstable outputs.

You can use lower temperature values for tasks like classification, entity extraction, or question answering, and use higher temperature values for tasks like content or idea generation.

## Top-k and top-p

Top-k sampling and top-p sampling are methods for picking the output tokens by controlling the quality and diversity of the generated output.

### Top-k

**Top-k is currently not an active parameter but if you believe your outputs could improve with this parameter, contact your Writer representative**

**Top-k defaults to 40, but it accepts any integer between 1 and 50,400.**

Top-k refers to selecting the k tokens with the highest predicted probabilities and then randomly selecting one of these k tokens to generate the next word. This method generates outputs that are more likely to be accurate, but may be less diverse.

Top-k refers to the number of tokens that get sampled, sorted by probability with all tokens beneath the `k`-th token not sampled. A lower value can improve quality by removing the long tail of less likely tokens and making it less likely to go off topic.

### Top-p

**Top-p defaults to 1 but accepts any number between 0 and 1.**

Top-p refers to selecting the top p% of the most likely tokens based on the predicted probabilities of the model, and then randomly selecting one of the top p tokens to generate the next word.

Top-p is an alternative way of controlling the randomness of the generated text. Generally, top-p provides better control where your model generates text with accuracy and correctness.

\*\*When modifying top-p, make sure that temperature is `1`. As a corollary, if you are modifying temperature, make sure that you set top-p to the default of `1` \*\*

Modifying both temperature and top-p from their defaults at the same time can lead to undesired results. The model first performs top-p sampling to narrow down the word selection, and then applies the temperature to adjust the remaining probabilities. This process can produce outputs that become either too constrained or too random, depending on your chosen values.

To strike a balance between control and diversity in text generation, it's generally recommended to modify top-p values or temperature values based on your desired outcome. Experimenting with different values and observing the generated outputs can help you determine which parameter suits your specific needs.

## `best_of`

**`best_of` defaults to `1`, but it accepts any integer between `1` and `5`**

In the context of language models, `best_of` is usually used in the context of [beam search](https://en.wikipedia.org/wiki/Beam_search), which is an algorithm used for generating sequences of words. Beam search explores multiple possible word sequences and selects the most promising ones based on a scoring mechanism.

In beam search, the `best_of` value refers to the number of top candidates that get considered at each step of the generation process. For example, if you set `best_of` to `3`, the algorithm considers the top 3 candidates at each step and proceeds with further exploration.

The higher the `best_of` value, the more candidates the LLM considers, which can increase the quality and accuracy of the output as well as the computational time and cost of the generation.


# Choose a model
Source: https://dev.writer.com/home/models

Choose the right Palmyra model for your needs. Compare capabilities, context windows, and pricing for the Palmyra X6, X5, and X4 models.

Whether you use no-code, Agent Builder, or APIs, you need to choose a model. Here is an overview of the Palmyra models and their capabilities.

## Model comparison

The following table compares the capabilities, context windows, and pricing of the Palmyra X6, X5, and X4 models.

| Feature              | Palmyra X6                                                                                                        | Palmyra X5                                                                  | Palmyra X4                                                     |
| -------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Description**      | Agentic model for WRITER Agent and long-horizon enterprise workflows with sub-agents, grounding, and MCP tool use | General-purpose model with a 1M token context window and adaptive reasoning | Language model for complex tasks that require extended context |
| **API model ID**     | `palmyra-x6`                                                                                                      | `palmyra-x5`                                                                | `palmyra-x4`                                                   |
| **Bedrock model ID** | —                                                                                                                 | `writer.palmyra-x5-v1:0`                                                    | `writer.palmyra-x4-v1:0`                                       |
| **Pricing (Input)**  | \$2.00 / 1M tokens                                                                                                | \$0.60 / 1M tokens                                                          | \$2.50 / 1M tokens                                             |
| **Pricing (Output)** | \$8.00 / 1M tokens                                                                                                | \$6.00 / 1M tokens                                                          | \$10.00 / 1M tokens                                            |
| **Context window**   | 1M tokens                                                                                                         | 1M tokens                                                                   | 128k tokens                                                    |
| **Max output**       | 8,192 tokens                                                                                                      | 8,192 tokens                                                                | 4,096 tokens                                                   |

<Note>
  Palmyra X6 is the default model for [WRITER Agent](/home/pricing#writer-agent). Palmyra X6, X5, and X4 are available via API. Palmyra X5 and X4 are also available in No-code and Agent Builder. To analyze images, use [vision capabilities with Palmyra X5](/home/chat-with-images).
</Note>

## Model details

<AccordionGroup>
  <Accordion title="Palmyra X6" icon="comment">
    <Tabs>
      <Tab title="Overview">
        Palmyra X6 is Writer's default agentic model for [WRITER Agent](https://support.writer.com/article/235-ask-writer) and long-horizon enterprise workflows. It was co-developed with the product's orchestration layer. Capability scores in the Benchmarking tab measure the model in that combined system, which is how customers deploy it.

        Palmyra X6 can hold a single objective for up to 8 hours without supervision, planning, executing, verifying output, and delivering finished work rather than a draft that needs another pass.

        **Pricing**:

        * Input: \$2.00 per 1M tokens
        * Output: \$8.00 per 1M tokens

        **Context window**: 1M

        **Task economics** (median on Writer's internal evaluation of production customer tasks, n=22):

        * Cost per finished task: \$0.12
        * Latency per finished task: 26 seconds
      </Tab>

      <Tab title="Use cases and capabilities">
        Palmyra X6 is built for the agentic work marketing, revenue, and operations teams run in production, including grounded research, content generation, connector-driven actions, and multi-step playbooks.

        **WRITER Agent workflows**

        * **Connector-driven tasks**: Query CRM records, search Slack messages, update documents, and perform other actions through [prebuilt and custom connectors](/home/mcp-gateway)
        * **Multi-step automation**: Break complex requests into sequenced tool calls across multiple systems
        * **Grounded responses**: Pull context from Knowledge Graphs, documents, and connector data with retrieval and citation
        * **Playbooks**: Execute predefined multi-step workflows end to end

        **Agentic capabilities**

        * **Sub-agents**: Delegate work to spawned sub-agents and merge results back into the primary workflow
        * **MCP tool use**: Discover and call external tools through MCP connectors
        * **Content generation**: Draft and edit long- and short-form content to a brief
        * **Brand voice**: Apply brand voice and custom instructions to output
        * **Long-horizon execution**: Sustain multi-hour objectives with planning, self-testing, and correction loops
        * **Image analysis and generation**: Interpret input images and generate new images
        * **Presentations** *(early)*: Generate slide presentations from a prompt or source
      </Tab>

      <Tab title="Benchmarking">
        Writer evaluates Palmyra X6 on production customer tasks (marketing, revenue, research, and outreach), not abstract reasoning benchmarks alone. Each capability is scored 0 to 1 against a locked baseline (n=22, reference frozen 2026-06-07). Scores measure Palmyra X6 running in WRITER Agent.

        **Capability scores (Palmyra X6 in WRITER Agent)**

        | Capability                    | What it measures                                                | Score    |
        | ----------------------------- | --------------------------------------------------------------- | -------- |
        | Sub-agents                    | Delegating work to spawned sub-agents and merging results       | **0.86** |
        | Grounding and retrieval       | Knowledge Graph and document grounding, retrieval, and citation | **0.91** |
        | MCP tool use                  | Discovering and calling external tools through MCP connectors   | **0.88** |
        | Content generation            | Drafting and editing long- and short-form content to a brief    | **0.88** |
        | Playbooks                     | Executing predefined multi-step workflows end to end            | **0.84** |
        | Model and system awareness    | Knowing its identity, tools, scope, and when to safely refuse   | **0.97** |
        | Brand voice                   | Applying brand voice and custom instructions to output          | **0.92** |
        | Image analysis and generation | Interpreting input images and generating new images             | **0.80** |
        | Presentations *(early)*       | Generating slide presentations from a prompt or source          | **0.74** |

        Average capability score: **0.87**.

        **Domain benchmarks (raw model)**

        The scores below measure the Palmyra X6 model directly, not in WRITER Agent, on finance benchmarks:

        | Benchmark     | Score |
        | ------------- | ----- |
        | Finance Agent | 0.753 |
        | FinanceQA     | 0.673 |
        | FinanceBench  | 0.855 |
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Palmyra X5" icon="comment">
    <Tabs>
      <Tab title="Overview">
        Palmyra X5 is Writer's general-purpose model for building and scaling AI agents, featuring a 1 million token context window, adaptive reasoning, and strong speed and cost efficiency.

        **Pricing**:

        * Input: \$0.60 per 1M tokens
        * Output: \$6.00 per 1M tokens

        **Content window**: 1M
      </Tab>

      <Tab title="Use cases and capabilities">
        Palmyra X5's 1M token context window further streamlines enterprise workflows and unlocks complex, multi-step use cases that weren't possible before.

        **Multi-step agentic workflows**

        * **Support documentation**: Classify requests, assess urgency, assign a human review, stage updates in a CMS, and publish after approval.
        * **Fund reporting**: Streamline the analysis and preparation of detailed reports on the performance and status of investment funds, using reporting and research data pulled in from third-party systems
        * **Content lifecycle management**: Flag content that could be outdated, generate suggested revisions, and share them for human review.

        **Large data requirements**:

        * **Customer feedback analysis**: Analyze large volumes of customer feedback to identify common themes, summarize sentiments, and generate actionable insights.
        * **Research and development**: Process and summarize multiple technical reports, research papers, and experimental data, accelerating innovation and product development.
        * **Financial reporting**: Process and summarize annual reports, SEC filings, and market analysis reports together at once to extract financial data, identify trends, and generate executive summaries.
        * **Legal document analysis**: Analyze lengthy legal documents, including contracts, patents, and compliance reports, to identify key clauses, flag potential risks, and ensure regulatory compliance.
        * **Medical records analysis**: Analyze and summarize large files of medical records containing structured and unstructured data, including patient records, clinical trial reports, audit reports, and more.
      </Tab>

      <Tab title="Benchmarking">
        Palmyra X5 demonstrates robust performance across a suite of industry-standard benchmarks, showcasing its capabilities in reasoning, retrieval, and domain-specific tasks.

        * **BBH (Big-Bench Hard)**: Evaluates complex reasoning and compositional logic. Palmyra X5 achieves a competitive score of 70.99%, aligning closely with top-tier models.
        * **GPQA (Graduate-Level Google-Proof Q\&A)**: Assesses the model's ability to answer challenging, graduate-level questions in biology, physics, and chemistry that are resistant to simple lookup strategies. X5's score of 47.20% indicates strong performance in scientific reasoning tasks.
        * **MMLU\_PRO**: Focuses on professional-level knowledge across various domains such as law, medicine, and finance. Palmyra X5 scores 65.02%, demonstrating its suitability for enterprise applications in regulated sectors.
        * **MATH\_HARD**: Tests symbolic reasoning and multi-step problem-solving abilities. X5's score of 71.57% showcases its proficiency in handling complex analytical tasks.
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Palmyra X4" icon="comment">
    <Tabs>
      <Tab title="Overview">
        `palmyra-x4` is an advanced language model that excels in processing and understanding complex tasks. It's ideal for workflow automation, coding tasks, and data analysis.

        **Pricing**:

        * Input: \$2.50 per 1M tokens
        * Output: \$10.00 per 1M tokens

        **Content window**: 128k
      </Tab>

      <Tab title="Use cases and capabilities">
        * **Agents & actions**: Palmyra X4 acts as an advanced AI agent, capable of executing tasks beyond simple text generation by interacting with external systems like databases, applications, and other services. This enables it to perform real-time data updates and automate complex workflows.
        * **Retrieval-augmented generation (RAG)**: Equipped with RAG, Palmyra X4 can retrieve and incorporate relevant information from vast data sources, enhancing the model's accuracy and ensuring responses are always grounded in current, context-specific data.
        * **Code generation**: The model supports advanced code generation, enabling it to automate scripting and integrate seamlessly with various programming environments, optimizing workflows for technical teams.
        * **Tool calling**: Palmyra X4 is built to handle precise API interactions, allowing it to execute complex functions directly, making it a versatile tool for enterprise-level integrations and automated actions.
      </Tab>

      <Tab title="Benchmarking">
        Palmyra X4 consistently ranks at the top in structured output, API tool calling, and accuracy for complex, multi-step workflows.

        * **Top accuracy (ACC)**: Palmyra X4 achieves 78.76% accuracy in tool call identification and execution, leading the industry by nearly 20%.
        * **Structured call planning (AST)**: Palmyra X4 scores 87.93% in planning and organizing tool calls, accurately interpreting input, generating parameters, and sequencing steps.
        * **Execution performance (Exec)**: With an 88.27% score in executing tool calls, Palmyra X4 ranks highest in efficiently carrying out enterprise actions.
        * **Global benchmarks**: Palmyra X4 ranks in the world's top 10 on HELM Lite (86.1%) and HELM MMLU (81.3%), excelling across 57 subjects.
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

## Using models with the Writer API

<Tabs>
  <Tab title="Chat completions">
    To select the Palmyra model you want to use for a chat completion, specify the model ID in the [`model` parameter](/api-reference/completion-api/chat-completion#body-model) of the request.

    Below is an example of specifying the `palmyra-x5` model in a chat completion request.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --location 'https://api.writer.com/v1/chat' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
        "model": "palmyra-x5",
        "messages": [
          {
            "role": "user",
            "content": "Summarize GDPR compliance requirements for a cloud-based data storage provider"
          }
        ]
      }'
      ```

      ```python Python theme={null}
      from writerai import Writer

      # Initialize the client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      chat_response = client.chat.chat(
        model="palmyra-x5",
        messages=[
          {
            "role": "user",
            "content": "Summarize GDPR compliance requirements for a cloud-based data storage provider"
          }
        ]
      )

      print(chat_response.choices[0].message.content)
      ```

      ```javascript JavaScript theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const chat_response = await client.chat.chat({
        model: "palmyra-x5",
        messages: [
          {
            role: "user",
            content:
              "Summarize GDPR compliance requirements for a cloud-based data storage provider",
          },
        ],
      });

      console.log(chat_response.choices[0].message.content);
      ```
    </CodeGroup>

    You can also use [prebuilt tools](/home/tool-calling) within a general-purpose chat completion request:

    * [LLM tool](/home/model-delegation) to call another model
    * [Knowledge Graph tool](/home/kg-chat) to ask questions about a Knowledge Graph
    * [Translation tool](/home/translation-tool) to translate text
    * [Web search tool](/home/web-search-tool) to search the web

    To analyze images, use [vision capabilities with Palmyra X5](/home/chat-with-images) directly in a chat completion.
  </Tab>

  <Tab title="Text generation">
    To select the Palmyra model you want to use for text generation, specify the model ID in the [`model` parameter](/api-reference/completion-api/text-generation#body-model) of the request.

    Below is an example of specifying the `palmyra-x5` model in a text generation request.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --location 'https://api.writer.com/v1/completions' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
        "model": "palmyra-x5",
        "prompt": "Summarize GDPR compliance requirements for a cloud-based data storage provider"
      }'
      ```

      ```python Python theme={null}
      from writerai import Writer

      # Initialize the client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      text_generation = client.completions.create(
        model="palmyra-x5",
        prompt="Summarize GDPR compliance requirements for a cloud-based data storage provider"
      )

      print(text_generation.choices[0].text)
      ```

      ```javascript JavaScript theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const text_generation = await client.completions.create({
        model: "palmyra-x5",
        prompt:
          "Summarize GDPR compliance requirements for a cloud-based data storage provider",
      });

      console.log(text_generation.choices[0].text);
      ```
    </CodeGroup>
  </Tab>
</Tabs>

Get started with the Writer API by signing up for a free account and following the [API quickstart](/home/quickstart).

## Deprecation policy

### Timeline for deprecation

* We'll announce the deprecation of a model at least three months in advance. This will give customers time to plan for the migration to the new model.
* We'll continue to support deprecated models for a period of time after they're deprecated. This will give customers time to migrate to the new model.
* We'll eventually stop supporting deprecated models. The timeline for this will vary depending on the model. We will announce the end of support for a deprecated model at least six months in advance.

### Deprecated models

| Model name             | Deprecation Date |
| ---------------------- | ---------------- |
| Palmyra X 003 Instruct | 2026-07-13       |
| Palmyra Vision         | 2026-07-13       |
| Palmyra Med            | 2026-07-13       |
| Palmyra Fin            | 2026-07-13       |
| Palmyra Creative       | 2026-07-13       |
| Palmyra Fin 32k        | 2025-03-03       |
| Palmyra X 002 32k      | 2024-09-06       |
| Palmyra X 32k Instruct | 2024-09-06       |
| Palmyra X 002 Instruct | 2024-09-06       |

### Migration path

* We'll provide customers with a migration path to the new model, including detailed documentation and support to help them migrate their applications.
* We'll also offer training and consulting services to help customers transition.


# Choose a model
Source: https://dev.writer.com/home/models-overview

Compare Writer's Palmyra models with external provider models to select the best fit for your AI agents

This guide helps you choose between Writer's Palmyra models and external provider models when building agents in AI Studio. Both options are available through the [Models page](https://app.writer.com/aistudio) in AI Studio.

## Palmyra models

Writer's [Palmyra models](/home/models) are pre-configured and immediately available in AI Studio. The Palmyra family includes:

| Model                                 | Best for                                                                                            | Available in                                    |
| ------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| [Palmyra X6](/home/models#palmyra-x6) | Long-horizon agentic workflows, WRITER Agent, sub-agents, grounding, and MCP tool use               | [WRITER Agent](/home/pricing#writer-agent), API |
| [Palmyra X5](/home/models#palmyra-x5) | General-purpose tasks with advanced reasoning, including financial, medical, and creative use cases | Agent Builder, no-code, API                     |
| [Palmyra X4](/home/models#palmyra-x4) | Complex tasks requiring extended context                                                            | Agent Builder, no-code, API                     |

For detailed capabilities, context windows, and use cases, see [Palmyra models](/home/models). For pricing, see [Palmyra pricing](/home/pricing).

Palmyra X6 is the default model for [WRITER Agent](/home/pricing#writer-agent). Palmyra X6, X5, and X4 are available via API. Palmyra X5 and X4 are also available in Agent Builder and no-code apps.

## External models

You can also add models from external providers like AWS Bedrock or Microsoft Azure (Azure OpenAI) to use alongside Palmyra models. External models require credential configuration but provide access to models like Claude, Llama, Mistral, and Azure-hosted OpenAI models.

| Provider        | Status      | Setup guide                                             |
| --------------- | ----------- | ------------------------------------------------------- |
| AWS Bedrock     | Available   | [Configure AWS Bedrock](/providers/aws-bedrock)         |
| Microsoft Azure | Available   | [Configure Microsoft Azure](/providers/microsoft-azure) |
| Baseten         | Coming soon | —                                                       |

For information about adding and managing external models, see [Add external models](/home/external-models).

## Compare Palmyra and external models

Consider the following factors when selecting models for your agents:

| Consideration           | Palmyra models                                                                | External models                                                                                          |
| ----------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| **Setup**               | Pre-configured, immediately available                                         | Requires credential configuration                                                                        |
| **Optimization**        | Optimized for Writer platform                                                 | Varies by provider                                                                                       |
| **Specialized domains** | Palmyra X5 covers general-purpose, financial, medical, and creative use cases | General-purpose models                                                                                   |
| **Compliance**          | Writer-managed compliance                                                     | You manage provider compliance                                                                           |
| **Billing**             | Unified billing through Writer                                                | Provider rate card plus a 10% WRITER pass-through fee. See [Add external models](/home/external-models). |
| **Cost visibility**     | Included in Writer dashboard                                                  | Tracked in Writer + provider billing                                                                     |

### When to use Palmyra models

Palmyra models are the best choice when you need:

* **Domain-specific capabilities**: Palmyra X5 covers general-purpose, financial, medical, and creative use cases
* **Simplified billing**: All usage appears on your Writer invoice
* **Writer platform features**: Optimized integration with Knowledge Graphs and other Writer capabilities
* **Compliance simplicity**: Writer manages model compliance and security

### When to use external models

External models are the best choice when you need:

* **Existing provider agreements**: You have AWS credits or enterprise agreements with cloud providers
* **Specific model access**: You need a particular model not available through Writer (for example, Claude, Llama, or Mistral on Bedrock)
* **Provider-specific compliance**: Your organization mandates specific cloud providers for AI workloads
* **Model comparison**: You want to evaluate performance across different model providers

For information about adding and managing external models, see [Add external models](/home/external-models).

## Use models in your applications

Palmyra models are available on different Writer surfaces depending on the model:

### WRITER Agent

[WRITER Agent](https://support.writer.com/article/235-ask-writer) uses [Palmyra X6](/home/models#palmyra-x6) (`palmyra-x6`) by default.

### Agent Builder and no-code agents

In [Agent Builder](/agent-builder/overview) and [no-code chat apps](/no-code/introduction), select a model from the **Model** dropdown. Palmyra X5 and X4 appear in the list, along with any external models your team has access to. Palmyra X6 is not available in Agent Builder or no-code apps.

### API usage

Palmyra X5 and X4 are available via the API. Use the [List models](/api-reference/completion-api/list-models) endpoint to see all models available to your organization. You can then pass the model ID in any request that supports the `model` parameter.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.writer.com/v1/models \
    -H "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python Python theme={null}
  from writerai import Writer

  client = Writer()

  models = client.models.list()
  for model in models.models:
      print(model.id)
  ```

  ```javascript JavaScript theme={null}
  import Writer from "writer-sdk";

  const client = new Writer();

  const models = await client.models.list();
  models.models.forEach((model) => console.log(model.id));
  ```
</CodeGroup>

To use a model, pass its ID to any completion endpoint. External models use the same API as Palmyra models—just swap the model ID:

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
    -H "Authorization: Bearer $WRITER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "anthropic.claude-3-sonnet-20240229-v1:0",
      "messages": [{"role": "user", "content": "Hello!"}]
    }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  client = Writer()

  response = client.chat.chat(
      model="anthropic.claude-3-sonnet-20240229-v1:0",
      messages=[{"role": "user", "content": "Hello!"}]
  )
  print(response.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import Writer from "writer-sdk";

  const client = new Writer();

  const response = await client.chat.chat({
    model: "anthropic.claude-3-sonnet-20240229-v1:0",
    messages: [{ role: "user", content: "Hello!" }],
  });
  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

## Next steps

* [Add external models](/home/external-models): Configure external models from providers like AWS Bedrock
* [Palmyra models](/home/models): Explore Writer's Palmyra model capabilities


# Observability overview
Source: https://dev.writer.com/home/observability

Monitor events, errors, and performance across your AI Studio deployment

AI Studio provides observability tools to help DevOps teams, site reliability engineers, and platform engineers monitor agent activity, debug issues, and maintain compliance. Use the built-in tools directly in AI Studio, or connect to external platforms your team already uses.

## Built-in observability

AI Studio includes the following observability features out of the box:

<CardGroup>
  <Card title="Event logs" icon="list-timeline" href="/home/event-logs">
    View a real-time log of platform events including API requests, errors, and
    agent interactions.
  </Card>

  <Card title="Agent observability" icon="gauge-high" href="/home/agent-observability">
    Monitor individual agent performance with session logs, engagement metrics,
    and usage data.
  </Card>

  <Card title="Usage and spend" icon="chart-pie" href="/home/usage-and-spend">
    Track organization-wide costs, token consumption, and spending trends.
  </Card>
</CardGroup>

## External observability

To stream logs and metrics to monitoring platforms your organization already uses, configure an observability plugin. Plugins forward request-level data from AI Studio to external tools, giving you centralized visibility alongside the rest of your infrastructure.

<Card title="Configure plugins" icon="plug" href="/home/plugins-overview">
  Set up integrations with platforms like Datadog to stream logs, metrics, and
  traces from AI Studio.
</Card>

## Next steps

* [Event logs](/home/event-logs): Monitor platform activity in real time
* [Agent observability](/home/agent-observability): Debug and optimize individual agent performance
* [Usage and spend](/home/usage-and-spend): Track organization-wide costs and token consumption
* [Configure plugins](/home/plugins-overview): Stream telemetry data to external monitoring platforms


# Parse a PDF
Source: https://dev.writer.com/home/parse-pdf



<Warning>
  **Deprecation notice**: The [parse PDF API endpoint](/api-reference/tool-api/pdf-parser) at `/v1/tools/pdf-parser/{file_id}` is deprecated and will be removed on **December 22, 2025**.

  **Migration path**: We plan to introduce a prebuilt PDF parsing tool for chat completions that will provide similar functionality. This tool will work similarly to other prebuilt tools like the [LLM tool](/home/model-delegation). We will provide more details about this alternative when it becomes available.
</Warning>

The [PDF parser endpoint](/api-reference/tool-api/pdf-parser) converts PDF documents into other formats. This is useful when you need to extract and process text content from PDF files for further analysis or integration into your workflow.

## Use cases

* Converting research papers from PDF to searchable text for analysis
* Extracting content from business reports for data processing
* Converting PDF documentation into markdown format for web publishing
* Making archived PDF documents searchable and analyzable
* Automating data extraction from PDF forms and invoices

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Endpoint overview

**URL:** `POST https://api.writer.com/v1/tools/pdf-parser`

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/tools/pdf-parser/<file-id>' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
    "format": "markdown"
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.tools.parse_pdf(
      file_id="file_id",
      format="text",
  )
  print(response.content)
  ```

  ```javascript JavaScript theme={null}
  import Writer from 'writer-sdk';

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  async function main() {
    const response = await client.tools.parsePdf('file_id', { format: 'text' });

    console.log(response.content);
  }

  main();
  ```
</CodeGroup>

### Path parameters

| Parameter | Description                               |
| --------- | ----------------------------------------- |
| `file_id` | The ID of the uploaded PDF file to parse. |

Before using the PDF parser, you'll need to upload your PDF file to the Writer API and obtain its file ID. Learn more about how to upload files with the [Files API](/home/files).

### Request body

The request body includes the following parameters:

| Parameter | Type     | Description                                             |
| --------- | -------- | ------------------------------------------------------- |
| `format`  | `string` | The desired output format. Can be `text` or `markdown`. |

### Response parameters

Returns an object with a `content` field containing the extracted text in the specified format.

```json JSON theme={null}
{
  "content": "..."
}
```


# Configure plugins
Source: https://dev.writer.com/home/plugins-overview

Stream telemetry and security data from AI Studio to your existing monitoring platforms with observability and security plugins.

This page covers how to configure plugins in AI Studio. Plugins enable your organization to stream telemetry and security data to existing monitoring platforms, giving platform admins, security ops, and data ops teams visibility into AI Studio activity from the tools they already use.

AI Studio supports two categories of plugins:

* **Observability plugins**: Stream logs and metrics to external monitoring platforms for centralized analysis.
* **Security plugins**: Connect third-party guardrails providers to AI Studio. Admins enable a security plugin on the Plugins page, which allows individual guardrails to be created and configured on the [Guardrails page](/home/guardrails).

<Note>
  Plugins are available on enterprise plans. Org admins, IT admins, and users
  with AI Studio full access roles can add and manage plugins.
</Note>

<img alt="Plugins page in AI Studio showing list of configured plugins." />

## How plugins work

Plugins stream monitoring data from AI Studio directly into your existing observability and security tools, giving you centralized visibility across every agent, interaction, and cost in your deployment.

With plugins configured, admins can:

* **Monitor agent activity** in the same dashboards and alerting workflows used across the rest of the organization
* **Audit agent behavior** by streaming request-level metadata to external logging and compliance tools
* **Enforce security policies** by routing agent interactions through third-party guardrails providers

```mermaid theme={null}
sequenceDiagram
    participant Client
    participant LLMGateway as AI Studio Gateway
    participant Provider as LLM Provider
    participant Plugin as Plugin Provider

    Client->>LLMGateway: Send chat/completion request
    LLMGateway->>Provider: Forward to model provider
    Provider-->>LLMGateway: Return model response
    LLMGateway-->>Client: Return response to client
    LLMGateway--)Plugin: Send request + response log<br/>(model, usage, status, metadata)
```

## Available plugins

AI Studio supports the following plugins. Select an available plugin to view detailed configuration instructions.

| Plugin           | Category      | Status      | Plugin docs                             |
| ---------------- | ------------- | ----------- | --------------------------------------- |
| **Datadog Logs** | Observability | Available   | [View docs](/home/plugins/datadog-logs) |
| OpenTelemetry    | Observability | Coming soon | —                                       |
| Lakera           | Security      | Coming soon | —                                       |
| Lasso            | Security      | Coming soon | —                                       |
| Noma             | Security      | Coming soon | —                                       |

## Set up a plugin

To configure a security or observability plugin:

1. Navigate to the **Plugins** page in [AI Studio](https://app.writer.com/aistudio)
2. Select **+ Add plugin**
3. Select the security or observability plugin you want to configure
4. Enter the required credentials (see the [individual plugin docs](#available-plugins) for details)
5. Configure any provider-specific settings (see the [individual plugin docs](#available-plugins) for details)

For detailed setup instructions, see the plugin-specific documentation linked in the [available plugins](#available-plugins) table.

## Next steps

* [Datadog Logs plugin](/home/plugins/datadog-logs): Configure log forwarding to Datadog
* [Observability overview](/home/observability): Monitor events, errors, and performance across your deployment
* [Agent observability](/home/agent-observability): Track individual agent performance with session logs and metrics


# Datadog Logs plugin
Source: https://dev.writer.com/home/plugins/datadog-logs

Send logs and metrics to Datadog for monitoring your AI Studio platform and agents.

This guide shows you how to configure the [Datadog](https://www.datadoghq.com/) Logs plugin for AI Studio. After setting up this plugin, AI Studio forwards a log event to Datadog for every LLM request. Each log event includes:

* **Model and routing metadata**: The model used and the API route that handled the request
* **Token usage**: Prompt tokens, completion tokens, and total tokens consumed per request
* **Performance data**: Success/failure status, latency, and error details when applicable
* **Cost**: Per-request spend attributed to the organization and model
* **Prompt and response content**: The full prompt and completion text, when available

<Note>
  If [content masking](/home/event-logs#mask-sensitive-content) is enabled in
  event logs, prompts and responses forwarded to Datadog are also masked.
</Note>

<img alt="Datadog Logs plugin example in Live tail logs." />

## Set up the Datadog Logs plugin

Configure the Datadog Logs plugin in [AI Studio](https://app.writer.com/aistudio) under **Plugins**.

### Obtain your Datadog API key

Before configuring the plugin in AI Studio, locate your API key in Datadog:

1. Log in to your [Datadog account](https://app.datadoghq.com/)
2. Navigate to **Organization Settings** → **API Keys**
3. Copy an existing API key or create a new one

For more information on obtaining a Datadog API key, see [Datadog API keys documentation](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).

### Identify your Datadog site region

Determine which Datadog site your organization uses. The **Site** hostname value controls which regional endpoint receives your logs.

| Region        | Site hostname value | Location     |
| ------------- | ------------------- | ------------ |
| US1 (default) | `datadoghq.com`     | US           |
| US3           | `us3.datadoghq.com` | US           |
| US5           | `us5.datadoghq.com` | US           |
| EU            | `datadoghq.eu`      | EU (Germany) |

For more information on Datadog regions, see [Datadog regions documentation](https://docs.datadoghq.com/getting_started/site/#access-the-datadog-site).

### Optional: Configure tags

Tags can be a standalone `value` or a `key:value` pair that Datadog attaches to your log events for filtering, aggregating, and grouping logs in dashboards, monitors, and alerts.

The following table shows common tag examples:

| Tag                            | Purpose                                |
| ------------------------------ | -------------------------------------- |
| `env:prod`                     | Identifies the deployment environment  |
| `instance:i-1234567890abcdef0` | Identifies a specific host or instance |
| `name:example-name`            | Labels the resource by name            |
| `managed`                      | Identifies logs from managed services  |
| `writer`                       | Groups all Writer-related log events   |

For more information on tagging requirements and strategies, see the [Datadog tagging documentation](https://docs.datadoghq.com/getting_started/tagging/).

### Configure the plugin in AI Studio

After obtaining your API key and identifying your site region:

1. Navigate to [AI Studio](https://app.writer.com/aistudio)
2. Open the Datadog Logs plugin configuration
3. Enter your configuration values:

| Field           | Description                                                                                                                                                                                    | Example                                            |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| Service name    | A stable, human-readable identifier for the service emitting logs. Maps to the reserved `service` attribute in the log payload. Datadog uses this to group logs and correlate with APM traces. | `writer-agent`, `writer-ai-studio`                 |
| Site hostname   | The Datadog regional intake endpoint. See [supported regions](#identify-your-datadog-site-region).                                                                                             | `datadoghq.com`                                    |
| (Optional) Tags | A list of `key:value` tags applied to every log event. Sent as the reserved `ddtags` attribute in the log payload. See [configure tags](#optional-configure-tags).                             | `env:prod`, `team:platform`, `component:ai-studio` |
| API key         | Your Datadog API key. See [obtain your API key](#obtain-your-datadog-api-key).                                                                                                                 | Your API key from Datadog                          |

4. Select **Save** to activate the plugin

<img alt="Datadog Logs plugin configuration in AI Studio." />

After saving, AI Studio begins forwarding log events to Datadog. Open the [Datadog Logs Explorer](https://app.datadoghq.com/logs) to verify that events are arriving, then build dashboards, monitors, and alerts around the data.

## Next steps

* [Observability overview](/home/observability): Monitor events, errors, and performance across your deployment
* [Agent observability](/home/agent-observability): Track individual agent performance with session logs and metrics
* [Configure plugins](/home/plugins-overview): Manage plugins across your organization


# Pricing
Source: https://dev.writer.com/home/pricing

View pricing for Writer AI models. Compare costs per million tokens for the Palmyra X6, X5, and X4 models.

<Info>For any custom pricing, **[contact Sales](https://go.writer.com/contact-sales)**.</Info>

## Base model

The table below outlines our [base model](/home/models#models), which is focused on text input and output. The input and output prices are displayed per 1M tokens unless otherwise specified.

| Model name                            | Model ID     | Input / 1M | Output / 1M |
| ------------------------------------- | ------------ | ---------- | ----------- |
| [Palmyra X6](/home/models#palmyra-x6) | `palmyra-x6` | \$2.00     | \$8.00      |
| [Palmyra X5](/home/models#palmyra-x5) | `palmyra-x5` | \$0.60     | \$6.00      |
| [Palmyra X4](/home/models#palmyra-x4) | `palmyra-x4` | \$2.50     | \$10.00     |

## WRITER Agent

[WRITER Agent](https://support.writer.com/article/235-ask-writer) usage is billed at the rate of the model used in each session. [Palmyra X6](/home/models#palmyra-x6) (`palmyra-x6`) is the default agent model. Admins can enable additional WRITER-managed models from the **Models** page in [AI Studio](https://app.writer.com/aistudio). Input and output rates for each model are shown there before an admin enables it.

Model usage in WRITER Agent is billed as follows:

|                      | WRITER-managed                                                                                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Models**           | Palmyra and [WRITER-managed third-party models](#writer-managed-third-party-models) enabled from the WRITER catalog                                                |
| **Provider account** | Not required                                                                                                                                                       |
| **Billing**          | Metered per token through WRITER (1 credit = 1 USD) at the rate shown for each model. See [WRITER-managed third-party models](#writer-managed-third-party-models). |
| **Usage visibility** | WRITER dashboard                                                                                                                                                   |
| **Availability**     | Included with WRITER-managed model access                                                                                                                          |

Connector usage, Knowledge Graph, and other WRITER Agent capabilities are priced under their own sections on this page, not as part of model token rates.

## WRITER-managed third-party models

The table below lists token rates for third-party models in the WRITER catalog. These models are WRITER-managed and available in WRITER Agent only. WRITER meters usage and bills you at the rates shown here.

<Warning>Rates are subject to change. Last updated: 7/20/26.</Warning>

| Model name                 | Model ID                            | Input / 1M | Output / 1M |
| -------------------------- | ----------------------------------- | ---------- | ----------- |
| Claude Opus 4.5            | `anthropic/claude-opus-4.5`         | \$5.00     | \$25.00     |
| Claude Opus 4.7            | `anthropic/claude-opus-4.7`         | \$5.00     | \$25.00     |
| Claude Sonnet 4.6          | `anthropic/claude-sonnet-4.6`       | \$3.00     | \$15.00     |
| Claude Sonnet 5            | `anthropic/claude-sonnet-5`         | \$2.00     | \$10.00     |
| DeepSeek V4 Flash          | `deepseek/deepseek-v4-flash`        | \$0.09     | \$0.18      |
| DeepSeek V4 Pro            | `deepseek/deepseek-v4-pro`          | \$0.44     | \$0.87      |
| Gemini 2.5 Flash           | `google/gemini-2.5-flash`           | \$0.30     | \$2.50      |
| Gemini 3 Flash Preview     | `google/gemini-3-flash-preview`     | \$0.50     | \$3.00      |
| Gemini 3 Pro Image Preview | `google/gemini-3-pro-image-preview` | \$2.00     | \$12.00     |
| Gemini 3.1 Pro Preview     | `google/gemini-3.1-pro-preview`     | \$2.00     | \$12.00     |
| MiniMax M2.5               | `minimax/minimax-m2.5`              | \$0.15     | \$0.90      |
| Kimi K2.5                  | `moonshotai/kimi-k2.5`              | \$0.38     | \$2.03      |
| Kimi K2.6                  | `moonshotai/kimi-k2.6`              | \$0.66     | \$3.41      |
| GPT 5.4 Image 2            | `openai/gpt-5.4-image-2`            | \$8.00     | \$15.00     |
| GPT 5.4 Nano               | `openai/gpt-5.4-nano`               | \$0.20     | \$1.25      |
| GPT OSS 120B               | `openai/gpt-oss-120b`               | \$0.04     | \$0.18      |
| Palmyra X5                 | `writer/palmyra-x5`                 | \$0.60     | \$6.00      |
| GLM 4.7                    | `z-ai/glm-4.7`                      | \$0.40     | \$1.75      |
| GLM 5                      | `z-ai/glm-5`                        | \$0.60     | \$1.92      |
| GLM 5.1                    | `z-ai/glm-5.1`                      | \$0.98     | \$3.08      |

## Palmyra Vision (deprecated)

The table below outlines pricing for Palmyra Vision, which takes in a variety of inputs and produces text as an output. The input and output price is for 1M tokens unless otherwise specified. Palmyra Vision is deprecated. Use [chat with images](/home/chat-with-images) with Palmyra X5 instead.

| Type  | Input          | Output / 1M |
| ----- | -------------- | ----------- |
| Image | \$0.005/image  | \$8.00      |
| Video | \$0.005/second | \$8.00      |
| Text  | \$7.50/1M      | \$22.50     |

## Knowledge Graph

The table below outlines pricing for [Knowledge Graph](/home/knowledge-graph), Writer's graph-based RAG.

| Capability              | Cost                          |
| ----------------------- | ----------------------------- |
| Knowledge Graph hosting | \$0.085/gb of storage per day |
| Data extraction         | \$0.00015/page                |
| File parsing (OCR)      | \$0.055/page                  |
| Web Access              | \$0.12/page                   |

Data connectors for Knowledge Graph are available for Enterprise plans. To learn, more please [contact Sales](https://go.writer.com/contact-sales).

## Tools API

The table below outlines pricing for the [Tools API](/api-reference/tool-api/), which provides utilities for pre-processing and comprehending text.

| Tool                                             | Cost         |
| ------------------------------------------------ | ------------ |
| [PDF parser](/api-reference/tool-api/pdf-parser) | \$0.055/page |

## Deprecated models

The table below show pricing for deprecated models. See the [deprecation policy](/home/models#deprecation-policy) for more information.

| Model name             | Deprecation date | Input / 1M                              | Output / 1M                             |
| ---------------------- | ---------------- | --------------------------------------- | --------------------------------------- |
| Palmyra X 003 Instruct | 2026-07-13       | \$7.50                                  | \$22.50                                 |
| Palmyra Vision         | 2026-07-13       | [See above](#palmyra-vision-deprecated) | [See above](#palmyra-vision-deprecated) |
| Palmyra Med            | 2026-07-13       | \$5.00                                  | \$12.00                                 |
| Palmyra Fin            | 2026-07-13       | \$5.00                                  | \$12.00                                 |
| Palmyra Creative       | 2026-07-13       | \$5.00                                  | \$12.00                                 |
| Palmyra Fin 32k        | 2025-03-03       | \$5.00                                  | \$12.00                                 |
| Palmyra X 002 32k      | 2024-09-06       | \$1.00                                  | \$2.00                                  |
| Palmyra X 32k Instruct | 2024-09-06       | \$1.00                                  | \$2.00                                  |
| Palmyra X 002 Instruct | 2024-09-06       | \$1.00                                  | \$2.00                                  |


# Private endpoints
Source: https://dev.writer.com/home/private-endpoints

Connect WRITER Agent to connectors over a private network using AWS PrivateLink or GCP Private Service Connect

A private endpoint is a network path between Writer's MCP gateway and a service in your VPC. Instead of sending connector traffic over the public internet, Writer connects through [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/privatelink/what-is-privatelink.html) or [GCP Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect) so requests stay on your cloud provider's private network.

<Note>
  For prebuilt connectors, contact your customer success manager to enable
  private endpoints. You can enable private endpoints on custom connectors
  during connector setup in AI Studio.
</Note>

## Prerequisites

Before setting up a private endpoint, make sure you have:

* Administrator access to your Writer organization
* Administrator access to your AWS or GCP account
* A service running in your VPC that you want to expose to WRITER Agent

## Create a private endpoint in your cloud provider

The first step is to create a private endpoint service in AWS or GCP that exposes your service to Writer.

### AWS PrivateLink

Create a VPC endpoint service in AWS that allows Writer to connect to your service privately.

1. Follow the AWS documentation to [create an endpoint service](https://docs.aws.amazon.com/vpc/latest/privatelink/create-endpoint-service.html)
2. When configuring allowed principals, add the Writer AWS account so that Writer can connect to your endpoint service
3. Copy the **Service name** (for example, `com.amazonaws.vpce.us-east-1.vpce-svc-xxxxxxxx`) for use in AI Studio

### GCP Private Service Connect

Create a published service in GCP that allows Writer to connect to your service privately.

1. Follow the GCP documentation to [configure Private Service Connect as a producer](https://cloud.google.com/vpc/docs/configure-private-service-connect-producer)
2. When configuring accepted consumers, add the Writer GCP project so that Writer can connect to your service
3. Copy the **Service attachment URI** (for example, `projects/PROJECT/regions/REGION/serviceAttachments/SERVICE_ATTACHMENT`) for use in AI Studio

## Add the private endpoint in AI Studio

After creating the endpoint service in your cloud provider, add it in AI Studio so it's available when configuring connectors.

1. Navigate to **Connectors & Tools** in [AI Studio](https://app.writer.com/aistudio)
2. Select **Private endpoints**
3. Select **Add private endpoint**
4. Enter the following details:
   * **Name**: A descriptive name for the private endpoint
   * **Cloud provider**: Select **AWS** or **GCP**
   * **Region**: The region where your endpoint service is hosted
   * **Service name** (AWS) or **Service attachment URI** (GCP): The identifier you copied from your cloud provider
5. Select **Save** to add the private endpoint

The new private endpoint appears in your list and is available when you configure connectors.

## Use a private endpoint with a custom connector

After you add a private endpoint in AI Studio, attach it when you create or edit a [custom connector](/home/custom-connectors):

1. Create or edit a custom connector in AI Studio under **Connectors & Tools**
2. In the **Connect** step, toggle **Enable private endpoint** on
3. Select the private endpoint you added
4. Complete the rest of the connector configuration

WRITER Agent routes requests for that connector through the selected private endpoint instead of the public internet.

## Next steps

* [Configure connectors](/home/mcp-gateway): Learn about connector authentication and access control
* [Custom connectors](/home/custom-connectors): Create custom connectors from OpenAPI specifications or MCP servers
* [Set up connectors](https://support.writer.com/article/299-setting-up-connectors): Configure prebuilt connectors in AI Studio
* [MCP gateway overview](/home/mcp-gateway): Learn about Writer's MCP gateway architecture


# Secure prompts against attacks
Source: https://dev.writer.com/home/prompt_injections



Security measures against prompt injections, jailbreak attacks, and secrets leakage

## Introduction

As AI systems like Writer models become increasingly integrated into a wide range of applications, the importance of securing them against various types of attacks becomes paramount. In this article, we delve into the technical details of how Writer safeguards its AI models against prompt injections, jailbreak attempts, and the leakage of sensitive information.

## Measures against prompt injections

<Tabs>
  <Tab title="Input sanitization">
    One of the first lines of defense is input sanitization, where incoming prompts are screened for potentially malicious code or harmful strings. Sophisticated Natural Language Processing (NLP) techniques, combined with standard cybersecurity measures, are used to detect and remove or neutralize suspicious inputs.
  </Tab>

  <Tab title="Rate limiting">
    To prevent brute-force attacks and other malicious activities, Writer often implements <a href="/api-reference/rate-limits">rate limiting</a> on the API requests. This ensures that a user can't overload the system with a large number of potentially harmful prompts in a short amount of time.
  </Tab>

  <Tab title="RBAC">
    Role-cased access control (RBAC) to more advanced or potentially risky functionalities is restricted based on roles. This prevents unauthorized users from injecting malicious prompts that could potentially exploit these functionalities.
  </Tab>
</Tabs>

## Safeguards against jailbreak attacks

<Tabs>
  <Tab title="Runtime environment isolation">
    The runtime environment in which the AI models operate is isolated from the rest of the system. This isolation is often achieved using containerization technologies like Docker, which limit the resources and system calls available to the running model.
  </Tab>

  <Tab title="Anomaly detection systems">
    Advanced anomaly detection systems are put in place to monitor the behavior of the AI models in real-time. Any unusual patterns or inconsistencies could trigger alerts, initiating immediate investigation and potential shutdown of the affected instance.
  </Tab>

  <Tab title="Code review and static analysis">
    Before deployment, the codebase undergoes rigorous reviews and static analysis to ensure that there are no vulnerabilities that could be exploited to "jailbreak" the AI system.
  </Tab>
</Tabs>

## Measures against secrets leakage

<Tabs>
  <Tab title="Encryption and secure storage">
    All sensitive information, including security keys and credentials, is encrypted using state-of-the-art encryption algorithms. These encrypted secrets are stored in secure vaults that are accessible only to authorized personnel.
  </Tab>

  <Tab title="Zero trust architecture">
    A Zero trust architecture is employed, which means that every request or interaction with the AI system is treated as potentially malicious until proven otherwise. This adds an extra layer of security against unauthorized access and data leakage.
  </Tab>
</Tabs>

## Conclusion

Security is a multi-faceted challenge that requires a holistic approach. Writer employs a combination of advanced technologies and best practices to safeguard its AI models against prompt injections, jailbreak attacks, and the leakage of sensitive information. While no system can be 100% secure, these measures provide a robust framework for identifying, mitigating, and responding to various security threats.


# Write effective prompts
Source: https://dev.writer.com/home/prompting

Master prompting fundamentals and advanced techniques. Learn directive writing, context inclusion, few-shot prompting, and chain-of-thought reasoning.

This guide covers the [fundamentals](/home/prompting#fundamentals) behind prompting strategies and discusses some [advanced techniques](/home/prompting#advanced-techniques).

For in-depth information on prompting, see the Writer [prompt crafting guide](https://writer.com/guides/prompt-crafting/).

## Fundamentals

<CardGroup>
  <Card title="Directive writing" icon="pencil">
    Be direct and to the point:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Inclusive prompts" icon="users">
    Include your audience in your prompt:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Structured formatting" icon="hashtag">
    Format your prompt with *### Instruction ###*, followed by *### Question
    \###*:

    <br />

    <br />

    <p>
      <strong>### Instruction ###</strong> <br />Write a poem in the style of
      Robert Frost. <br />

      <br />

      <strong>### Question ###</strong>
      <br /> What's the poem about?
    </p>
  </Card>

  <Card title="Task breakdown" icon="list-check">
    Break down complicated tasks into multiple prompts:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Positive language" icon="thumbs-up">
    Use directives like "do" instead of "don't":

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Mandatory instructions" icon="triangle-exclamation">
    Use phrases like *You MUST*:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Penalty warning" icon="gavel">
    Use phrases like *You'll be penalized*:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Natural responses" icon="comments">
    Request a simple response:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Unbiased descriptions" icon="scale-balanced">
    Make sure that your answer is unbiased and doesn’t rely on stereotype:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Educational writing" icon="book-open">
    To write an essay, text, paragraph, article, or any detailed text:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Style consistency" icon="typewriter">
    Improve the tone of the text to make the writing style more
    professional:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Creative writing" icon="pen-fancy">
    I'll give you the beginning of a story. Finish it and keep the flow
    consistent:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Linguistic mimicry" icon="copy">
    Use the same language as the following paragraph to explain the importance
    of exercise:

    <br />

    <br />

    <PromptComponent />
  </Card>

  <Card title="Precise instructions" icon="ruler">
    Clearly state the requirements that the model must follow to produce
    content, in the form of keywords or instructions. <br />

    <br />

    <PromptComponent />
  </Card>
</CardGroup>

## Advanced techniques

<CardGroup>
  <Card title="Example driven prompts" icon="check">
    Use example-driven (few-shot) prompting:

    <br />

    <p>
      Provide a list of idioms in English, along with their meanings and example
      sentences.

      <br />

      Here is an example for the output: <br />

      <br />

      <strong>An idiom</strong>: 'Break a leg' <br />
      <strong>Meaning</strong>: Good luck <br />
      <strong>Example sentence</strong>: 'I'm sure you'll do great in your interview.
      Break a leg!'
    </p>
  </Card>

  <Card title="Chain-of-thought Prompts" icon="thought-bubble">
    Combine chain-of-thought (CoT) prompts with few-shot prompts:

    <br />

    <p>
      <strong>System instruction:</strong> Create a product detail page for a
      fictional innovative smartphone by a retailer known as "TechTrend
      Electronics."

      <br />

      <strong>Prompt 1:</strong> Start by describing the unique features of the smartphone,
      such as its solar-powered battery, triple-lens camera system, and foldable
      screen technology.

      <br />

      <strong>Prompt 2:</strong> Next, outline the benefits of these features
      for users, like extended battery life, exceptional photo quality, and
      enhanced device portability.

      <br />

      <strong>Prompt 3:</strong> Conclude with crafting compelling product descriptions
      and a call-to-action that entices customers to make a purchase during the upcoming
      holiday sale.
    </p>
  </Card>

  <Card title="Delimited prompts" icon="text">
    Use delimiters to structure text:

    <br />

    <p>
      Summarize a series of healthcare claims documents for a fictional
      healthcare company, 'HealthFirst Solutions', using the following delimiter `\n`
      to separate different sections of the summary:

      <br />

      <strong>Claim Number:</strong> 123456789 `\n` <br />
      <strong>Date of Service:</strong> January 1, 2024 `\n` <br />
      <strong>Diagnosis:</strong> Acute sinusitis `\n` <br />
      <strong>Total Claimed:</strong> \$300 `\n` <br />
      <strong>Status:</strong> Pending review `\n` <br />
    </p>
  </Card>
</CardGroup>


# Quickstart
Source: https://dev.writer.com/home/quickstart

Make your first API call to Writer. Create an API key and generate text using the chat completion endpoint in minutes.

This guide helps you make your first API calls to the Writer API. You'll create an API key in [AI Studio](https://app.writer.com/aistudio) and begin making API calls to generate text using the [chat completion endpoint](/api-reference/completion-api/chat-completion).

## Get an API key

Before you can make API calls, you need to create an API key in [AI Studio](https://app.writer.com/aistudio).

<Steps>
  <Step title="Navigate to API Keys">
    From the [AI Studio home page](https://app.writer.com/aistudio), open **Admin settings** in the left sidebar, then select **API Keys**.

    <img alt="" />
  </Step>

  <Step title="Create an API agent">
    Select **Create API agent** in the top right corner of the page. In the **Create an API agent** dialog, under **Who can use this API key by default**, choose **All teams** or **Specific team**:

    * For **All teams**, select a **Key owner**:

      <img alt="" />

    * For **Specific team**, choose the **Team with immediate access** and select a **Key owner**:

      <img alt="" />

    Then select **Create API agent**.
  </Step>

  <Step title="Configure the agent">
    Select the title field for the API agent and give it a name. Add a **short description** so you can keep track of what the agent is for.

    <img alt="" />
  </Step>

  <Step title="Get your API key">
    On the **Manage keys** tab, each key is listed by name (the default is `Production`). The first time a key is created, copy the full value with **Copy** and store it in a secure place. You can't view or reveal the full key again after that.

    <Warning>Copy and store the key as soon as it's shown. After you leave this step, the full value is no longer available.</Warning>
  </Step>
</Steps>

<Tip>Store the API key in a secure location, such as a `.env` file.</Tip>

You can now make calls to the Writer API using this API key as the `Bearer` token in the header:

```
Authorization: Bearer <api-key>
```

Learn more about creating and managing API keys in the [API keys guide](/api-reference/api-keys).

## Make your first API call

Below is an example of an API call to the [chat completion endpoint](/home/chat-completion). It completes a single-turn chat completion, where the user provides a message and the AI assistant generates a response. The request is non-streaming and waits until the response is complete before returning.

```bash theme={null}
curl --location 'https://api.writer.com/v1/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your-api-key>' \
--data '{
    "model": "palmyra-x5",
    "messages": [
        {
            "role": "user",
            "content": "Write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions"
        }
    ]
}'
```

The response is a JSON object with a `choices` array. The `message.content` field of the first choice contains the generated text. The `message.role` field indicates that the message is an AI assistant message.

```json theme={null}
{
  "id": "78766762-bd30-4a42-bb2b-e0b35c608217",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "content": "This versatile, cozy sweater blends warmth and elegance with its soft knit and refined design, making it perfect for both casual days and formal evenings.",
        "role": "assistant",
        "tool_calls": null,
        "graph_data": {
          "sources": null,
          "status": null,
          "subqueries": null
        },
        "llm_data": null,
        "image_data": null,
        "refusal": null
      },
      "logprobs": null
    }
  ],
  "created": 1740711212,
  "model": "palmyra-x5",
  "usage": {
    "prompt_tokens": 50,
    "total_tokens": 79,
    "completion_tokens": 29,
    "prompt_token_details": null,
    "completion_tokens_details": null
  },
  "system_fingerprint": "v1",
  "service_tier": null
}
```

<Info>In a streaming response, the structure of the response is similar. However, the content of the streamed response is returned in a `choices[0].delta.content` field, rather than the `choices[0].message.content` field. Learn more about streaming responses in the [chat completion guide](/home/chat-completion).</Info>

You can customize your request to the chat completion endpoint by adding or updating parameters to the request body. Learn more about the different parameters in the [chat completion endpoint API reference](/api-reference/completion-api/chat-completion).

## Next steps

Now that you've made your first API call, learn how to use [the Writer SDKs](/home/sdks) to make calls to the Writer API in your applications. Or, learn more about building with the chat completion endpoint:

* Learn how to build a multi-turn, conversational AI app using the [chat completion guide](/home/chat-completion)
* See the full [chat completion endpoint API reference](/api-reference/completion-api/chat-completion)


# Research
Source: https://dev.writer.com/home/research





# SDKs
Source: https://dev.writer.com/home/sdks

Install and use Writer SDKs for Python and Node.js. Get started with code examples and learn how to integrate Writer AI into your applications.

This guide helps you get started with the Writer SDKs. Follow these steps to install the SDKs and perform basic operations.

### Prerequisites

<Tabs>
  <Tab title="Python SDK">
    * Python 3.7+
    * [pip](https://pypi.org/project/pip/)
    * A Writer API key
  </Tab>

  <Tab title="Node SDK">
    * Node.js
    * [`npm`](https://www.npmjs.com/get-npm)
    * A Writer API key
  </Tab>
</Tabs>

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

### Install the SDK

Open your terminal or command prompt and install the Writer SDK:

<CodeGroup>
  ```sh Python theme={null}
  pip install writer-sdk
  ```

  ```sh Node theme={null}
  npm install writer-sdk
  ```
</CodeGroup>

## Initialize the client

To initialize the client, import the Writer SDK and create an instance of the `Writer` class.

We recommend setting your API key in an environment variable called `WRITER_API_KEY`. When you initialize the Writer client, the client looks for the `WRITER_API_KEY` environment variable automatically. You can also pass your API key directly to the client.

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()
  ```

  ```js JavaScript theme={null}
  import { Writer } from 'writer-sdk';
  // Initialize the client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();
  ```
</CodeGroup>

## Make a chat completion request

Once you've initialized the client, you can make a request to the Writer API. The following example shows how to make a [chat completion request](/home/chat-completion) to generate a poem:

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  client = Writer()

  response = client.chat.chat(
      messages=[{ "content": "Write a short poem about Python", "role": 'user' }],
      model="palmyra-x5"
  )

  print(response.choices[0].message.content)
  ```

  ```js JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  const client = new Writer();

  const response = await client.chat.chat({
      messages: [{ content: "Write a short poem about JavaScript", role: 'user' }],
      model: "palmyra-x5"
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

## Next steps

Now that you're set up with the SDKs, start building with [chat completions](/home/chat-completion) or [text generation](/home/text-generation).

You can also use the [API reference](/api-reference) to learn more detailed information about available endpoints.


# Set up BYOK encryption
Source: https://dev.writer.com/home/setup-byok

Connect your own KMS from AWS, Azure, or GCP to control encryption keys for your Writer organization.

This guide walks you through enrolling your Writer organization in bring-your-own-key (BYOK) encryption. After completing these steps, your data is encrypted with a master key that you control in your own cloud KMS. Writer never sees or stores the key.

For an overview of encryption architecture, scopes, and key actions, see [Encryption Key Management](/home/encryption-key-management).

<Info>
  BYOK encryption is available on Enterprise plans. You must be an organization
  admin to configure encryption settings.
</Info>

<img alt="Encryption page in AI Studio showing Writer-managed encryption connected and BYOK configuration options for Azure Key Vault, AWS KMS, and GCP Cloud KMS." />

## Prerequisites

Before you begin, make sure you have:

* A Writer Enterprise plan with organization admin access
* An active KMS account with one of the supported providers:
  * [AWS KMS](https://aws.amazon.com/kms/)
  * [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault)
  * [GCP Cloud KMS](https://cloud.google.com/kms/docs)
* Permissions to create keys and manage access policies in your KMS

## Enroll in encryption

To enable encryption, navigate to the **Encryption** page under **Admin settings** in AI Studio. You can choose one of two options:

* **Enable Writer-managed encryption**: Writer manages the master key. Select this for zero-maintenance encryption.
* **Bring your own encryption keys**: Connect your own KMS from AWS, Azure, or GCP for direct control over the master key.

You can start with Writer-managed encryption and switch to BYOK at any time.

<Note>
  Enabling encryption applies only to new data created after enrollment.
  Pre-existing data is not retroactively encrypted.
</Note>

## Connect your KMS for BYOK

To set up BYOK, either during initial enrollment or switching from Writer-managed encryption:

1. In AI Studio, navigate to the **Encryption** page under **Admin settings**.
2. On the enrollment screen, select **Bring your own encryption keys**.
   * If encryption is already enabled, select the **Master key** tab and then select **Configure** under **Bring your own keys**.
3. Choose your cloud provider: **AWS KMS**, **Azure Key Vault**, or **GCP Cloud KMS**.
4. Follow the provider-specific instructions in the setup modal to create or select a key in your KMS and grant Writer access.
5. Paste your key identifier (ARN, key URL, or resource name) into the form. Writer validates the connection to your key.
6. After validation succeeds, the setup confirms that your BYOK configuration is complete.

## Control access from your KMS

With BYOK, you can control Writer's access to all encrypted data directly from your cloud provider's KMS console.

### Disable your CMK

Disabling your CMK immediately prevents Writer from unwrapping any DEKs across your entire organization. Use this during an active incident investigation when you need to cut off all access at once.

* In **AWS KMS**: Select the key and choose **Disable key**.
* In **Azure Key Vault**: Set the key's **Enabled** attribute to **false**.
* In **GCP Cloud KMS**: Select the key version and choose **Disable**.

Re-enabling the key restores Writer's ability to decrypt data.

### Delete your CMK

Deleting your CMK permanently prevents Writer from unwrapping any DEKs. All encrypted data across your organization becomes permanently inaccessible.

<Warning>
  Deleting your CMK is irreversible. There is no way for Writer or your cloud
  provider to restore access to your data after the key is deleted.
</Warning>

Most cloud providers enforce a waiting period before permanent deletion (for example, AWS KMS requires a 7-30 day waiting period). Use this window to verify that you intend to destroy access permanently.

## Next steps

* Review [Encryption Key Management](/home/encryption-key-management) for encryption architecture, scopes, and key actions
* [Configure plugins](/home/plugins-overview) to stream security telemetry alongside your KMS audit logs
* [Event logs](/home/event-logs) to monitor request-level activity across your organization


# Stream responses from the API
Source: https://dev.writer.com/home/streaming

Stream API responses in real-time with Server-Sent Events. Reduce latency and display generated content as it's created for better user experience.

The Writer API supports streaming responses from the API. Streams use [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent%5Fevents/Using%5Fserver-sent%5Fevents) so that you can display content as the API generates it in real time.

Streaming improves user experience by reducing latency from the time the user submits a request to the time the response is available.

## Overview

To stream a response from the API, set the `stream` parameter to `true` in the request body.

The following endpoints support streaming:

* [Text generation](/api-reference/completion-api/text-generation#body-stream)
* [Chat completions](/api-reference/completion-api/chat-completion#body-stream)
* [Generate from no-code agent](/api-reference/application-api/applications#body-stream): Currently, only no-code research agents support streaming.
* [Knowledge Graph question](/api-reference/kg-api/question#body-stream)
* [Web search](/home/web-search#streaming-response)

## Sample request and response

The code below shows a streaming request and response from a text generation request using `curl`. The response is a stream of [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent%5Fevents/Using%5Fserver-sent%5Fevents).

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/completions' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
    "model": "palmyra-x5",
    "prompt": "Respond to a customer chat request about a delayed shipment with a message that apologizes for the delay, offers a tracking number, and provides a new estimated delivery date.",
    "stream": true
  }'
  ```
</CodeGroup>

```json response theme={null}
data: {"value":""}

data: {"value":"Hello"}

data: {"value":" ["}

data: {"value":"Customer"}

data: {"value":"'s"}

data: {"value":" Name"}
...
```

<Note>
  If you're using the chat completions endpoint instead of text generation, the streamed response format is slightly different. The content for each chunk appears in `choices[0].delta.content`. See the [Generate chat completions](/home/chat-completion#streaming-response) guide for the full streaming response object.
</Note>

## Streaming with SDKs

When you stream a response using a Writer SDK, the SDK creates an iterator that yields chunks of the response. You can iterate over the stream to receive the response.

See below for examples of streaming with the Python and JavaScript SDKs for each endpoint that supports streaming.

<Tabs>
  <Tab title="Text Generation">
    <CodeGroup>
      ```python Python theme={null}
      from writerai import Writer

      # Initialize the client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      text_generation = client.completions.create(
        model="palmyra-x5",
        prompt="Respond to a customer chat request about a delayed shipment with a message that apologizes for the delay, offers a tracking number, and provides a new estimated delivery date.",
        stream=True
      )

      for chunk in text_generation:
          if chunk.value:  # Only print non-empty chunks
              print(chunk.value, end="", flush=True)
      ```

      ```javascript JavaScript theme={null}
      import { Writer } from 'writer-sdk';

      // Initialize the client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const stream = await client.completions.create({
        model: 'palmyra-x5',
        prompt: 'Respond to a customer chat request about a delayed shipment with a message that apologizes for the delay, offers a tracking number, and provides a new estimated delivery date.',
        stream: true 
      });

      for await (const chunk of stream) {
        process.stdout.write(chunk.value);
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Chat Completions">
    <CodeGroup>
      ```python Python theme={null}
      from writerai import Writer

      # Initialize the client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      stream = client.chat.chat(
        model="palmyra-x5",
        messages=[{"role": "user", "content": "Respond to a customer chat request about a delayed shipment with a message that apologizes for the delay, offers a tracking number, and provides a new estimated delivery date."}],
        stream=True
      )

      for chunk in stream:
          if chunk.choices[0].delta.content:
              print(chunk.choices[0].delta.content, end="", flush=True)
      ```

      ```javascript JavaScript     theme={null}
      import { Writer } from 'writer-sdk';

      // Initialize the client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const stream = await client.chat.chat({
        model: 'palmyra-x5',
        messages: [{ role: 'user', content: 'Respond to a customer chat request about a delayed shipment with a message that apologizes for the delay, offers a tracking number, and provides a new estimated delivery date.' }],
        stream: true 
      });

      for await (const chunk of stream) {
        if (chunk.choices[0]?.delta?.content) {
          process.stdout.write(chunk.choices[0].delta.content);
        }
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Generate from no-code agent">
    <CodeGroup>
      ```python Python theme={null}
      from writerai import Writer

      # Initialize the client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      stream = client.applications.generate_content(
          application_id="<application-id>",
          inputs=[
              {
                  "id": "query",
                  "value": ["Provide a list of three hotels in San Francisco near Union Square within the price range of $100 to $200 per night"]
              }
          ],
          stream=True
      )

      for chunk in stream:
          if chunk.delta.content:
              print(chunk.delta.content, end="", flush=True)
          elif chunk.delta.stages:
              print(chunk.delta.stages[0].content)
      ```

      ```javascript JavaScript theme={null}
      import { Writer } from 'writer-sdk';

      // Initialize the client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const stream = await client.applications.generateContent(
          "<application-id>", 
          { 
              inputs: [
                  {
                      id: "query",
                      value: ["Provide a list of three hotels in San Francisco near Union Square within the price range of $100 to $200 per night"]
                  }
              ],
              stream: true
          } 
      );

      for await (const chunk of stream) {
          if (chunk.delta.content) {
              process.stdout.write(chunk.delta.content);
          } else if (chunk.delta.stages) {
              process.stdout.write(chunk.delta.stages[0].content);
          }
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Knowledge Graph question">
    <CodeGroup>
      ```python Python theme={null}
      from writerai import Writer

      # Initialize the client. If you don't pass the `api_key` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      stream = client.graphs.question(
          graph_ids=["<graph-id>"],
          question="What is the generic name for the drug Bavencio?",
          stream=True
      )

      for chunk in stream:
          if chunk.answer:  # Only print non-empty answers
              print(chunk.answer, end="", flush=True)
      ```

      ```javascript JavaScript theme={null}
      import { Writer } from 'writer-sdk';

      // Initialize the client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      const stream = await client.graphs.question({
          graph_ids: ["<graph-id>"],
          question: "What is the generic name for the drug Bavencio?",
          stream: true
      });

      for await (const chunk of stream) {
          process.stdout.write(chunk.answer);
      }
      ```
    </CodeGroup>
  </Tab>
</Tabs>

### Streaming helpers for chat completions

The Python and Node SDKs include streaming helpers for chat completions. These helpers provide more granular details about the streaming events and accumulate the response.

To use the streaming helpers, call `client.chat.stream`. Then, include all the same parameters as you would for a non-streaming chat completion request, except omit the `stream` parameter.

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  with client.chat.stream(
    model="palmyra-x5",
    messages=[{"role": "user", "content": "Respond to a customer chat request about a delayed shipment with a message that apologizes for the delay, offers a tracking number, and provides a new estimated delivery date."}]
  ) as stream:
      for event in stream:
          if event.type == "content.delta":
              print(event.delta, end="", flush=True)

  # print the final response
  completion = stream.get_final_completion()
  print(completion.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const stream = client.chat.stream({
    model: 'palmyra-x5',
    messages: [{ role: 'user', content: 'Respond to a customer chat request about a delayed shipment with a message that apologizes for the delay, offers a tracking number, and provides a new estimated delivery date.' }],
  })
  .on('content', (diff) => process.stdout.write(diff));

  // print the final response
  const completion = await stream.finalChatCompletion();
  console.log(completion.choices[0].message.content);
  ```
</CodeGroup>

For more information about the streaming helpers for chat completions, see the [Python](https://github.com/writer/writer-python/blob/main/helpers.md) and [Node](https://github.com/writer/writer-node/blob/main/helpers.md) SDKs.


# Receive structured outputs
Source: https://dev.writer.com/home/structured-output



## Overview

The [`chat` endpoint](/api-reference/completion-api/chat-completion) supports structured outputs for chat completions with `palmyra-x5` and `palmyra-x4` models.

Structured outputs allow you to specify the response schema that the model should follow.

You can use structured outputs in one of two ways:

1. [Pass in a `json_schema` object](/home/structured-output#sample-request-and-response) in the `response_format` parameter when making a request to the `chat` endpoint.
2. [Use the `parse` method](/home/structured-output#sdk-parse-methods) in the Python and JavaScript SDKs and pass in a [`Pydantic`](https://docs.pydantic.dev/) or [`Zod`](https://zod.dev/) object.

## Sample request and response

The sample request and response below shows how to use structured outputs with the `chat` endpoint and the `response_format` parameter.

The `json_schema` object must be in [JSON Schema format](https://json-schema.org/understanding-json-schema/).

To guarantee that the model includes all the fields in the response, include the [`required` field](https://json-schema.org/understanding-json-schema/reference/object.html#required) in the `json_schema` object.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.writer.com/v1/chat \
    --header "Content-Type: application/json" \
    --header "Authorization: Bearer $WRITER_API_KEY" \
    --data '{
      "model": "palmyra-x5",
      "messages": [
        { "role": "system", "content": "You are a helpful cooking assistant." },
        { "role": "user", "content": "Give me a recipe for a simple pasta dish." }
      ],
      "response_format": {
        "type": "json_schema",
        "json_schema": {
          "schema": {
            "type": "object",
            "properties": {
              "recipe_name": {"type": "string"},
              "ingredients": {"type": "array", "items": {"type": "string"}},
              "steps": {"type": "array", "items": {"type": "string"}},
              "preparation_time": {"type": "string"},
              "servings": {"type": "number"}
            },
            "required": ["recipe_name", "ingredients", "steps", "preparation_time", "servings"]
          }
        }
      }
    }'
  ```

  ```python Python theme={null}
  import json
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.chat.chat(
    model="palmyra-x5",
    messages=[
      {"role": "system", "content": "You are a helpful cooking assistant."},
      {"role": "user", "content": "Give me a recipe for a simple pasta dish."}
    ],
    response_format={
      "type": "json_schema",
      "json_schema": {
        "schema": {
          "type": "object",
          "properties": {"recipe_name": {"type": "string"}, "ingredients": {"type": "array", "items": {"type": "string"}}, "steps": {"type": "array", "items": {"type": "string"}}, "preparation_time": {"type": "string"}, "servings": {"type": "number"}},
          "required": ["recipe_name", "ingredients", "steps", "preparation_time", "servings"]
        }
      }
    }
  )

  print(json.loads(response.choices[0].message.content))
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.chat.chat({
    model: 'palmyra-x5',
    messages: [
      { role: 'system', content: 'You are a helpful cooking assistant.' },
      { role: 'user', content: 'Give me a recipe for a simple pasta dish.' }
    ],
    response_format: { 
      type: 'json_schema',
      json_schema: {
        schema: {
          type: 'object',
          properties: { recipe_name: { type: 'string' }, ingredients: { type: 'array', items: { type: 'string' } }, steps: { type: 'array', items: { type: 'string' } }, preparation_time: { type: 'string' }, servings: { type: 'number' } },
          required: ['recipe_name', 'ingredients', 'steps', 'preparation_time', 'servings']
        }
      }
    },
  });

  console.log(JSON.parse(response.choices[0].message.content));
  ```
</CodeGroup>

The response from the `chat` endpoint includes a `message` object with the `content` field set to the raw response from the model.

<CodeGroup>
  ```json response theme={null}
  {
  "id": "1234",
  "object": "chat.completion",
  "choices": [
      {
      "index": 0,
      "finish_reason": "stop",
      "message": {
          "content": "{ \"recipe_name\": \"Spaghetti Aglio e Olio\", \"ingredients\": [\"12 oz spaghetti\", \"3-4 garlic cloves\", \"1/3 cup extra virgin olive oil\", \"4-6 dried red chili flakes (optional)\", \"Salt\", \"Freshly ground black pepper\", \"Grated Parmesan cheese (optional)\", \"Fresh parsley, chopped (optional)\"], \"steps\": [\"Bring a large pot of salted water to a boil. Cook the spaghetti according to the package instructions until al dente. Reserve 1 cup of pasta water before draining.\", \"In a large skillet, heat the olive oil over medium-low heat. Add the thinly sliced garlic and cook, stirring occasionally, until golden brown (about 4-5 minutes). Be careful not to burn the garlic.\", \"If using red chili flakes, add them to the skillet and cook for another minute.\", \"Add the reserved pasta water to the skillet and stir to combine.\", \"Add the cooked spaghetti to the skillet, tossing to coat the pasta in the garlic and oil mixture. Season with salt and black pepper to taste.\", \"Serve hot, topped with grated Parmesan cheese and chopped fresh parsley if desired.\"] }",
          "role": "assistant",
          "tool_calls": null,
          "graph_data": {
          "sources": null,
          "status": null,
          "subqueries": null
          },
          "llm_data": null,
          "image_data": null,
          "translation_data": null,
          "refusal": null
      },
      "logprobs": null
      }
  ],
  "created": 1745948916,
  "model": "palmyra-x5",
  "usage": {
      "prompt_tokens": 2550,
      "total_tokens": 2797,
      "completion_tokens": 247,
      "prompt_token_details": null,
      "completion_tokens_details": null
  },
  "system_fingerprint": "v1",
  "service_tier": null
  }
  ```
</CodeGroup>

## SDK parse methods

The Python and JavaScript SDKs provide helper `parse` methods for working with structured outputs.

You can define a `Pydantic` or `Zod` model for the response schema, and pass it as the `response_format` parameter to the `parse` method.

The result from the `parse` method includes a `message` object with the `parsed` field. This field's value is an instance of the `Pydantic` or `Zod` model you passed for the `response_format` parameter.

<Warning>
  If you include tool calls in your request, the `parse` method only works with [custom functions](/home/tool-calling) where the tool calls are of type `function`. It does not work with built-in tools with other types such as `web_search`, `vision`, or `translate`. For those cases, you can pass a `json_schema` object in the `response_format` parameter for the `chat` endpoint.
</Warning>

<CodeGroup>
  ```python Python theme={null}
  from typing import List
  from pydantic import Field, BaseModel
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  # Define a Pydantic model for structured output
  class Recipe(BaseModel):
      recipe_name: str = Field(description="The name of the recipe")
      ingredients: List[str] = Field(description="A list of ingredients for the recipe")
      steps: List[str] = Field(description="A list of steps for the recipe")
      preparation_time: str = Field(description="The preparation time for the recipe")
      servings: int = Field(description="The number of servings for the recipe")

  parsed_response = client.chat.parse(
      model="palmyra-x5",
      messages=[
          {"role": "system", "content": "You are a helpful cooking assistant."},
          {
              "role": "user",
              "content": "Give me a recipe for a simple pasta dish.",
          },
      ],
      response_format=Recipe,
  )
  parsed = parsed_response.choices[0].message.parsed

  # Print the parsed response
  print(f"Recipe name: {parsed.recipe_name}")
  print(f"Ingredients:")
  for item in parsed.ingredients:
      print(f"- {item}")
  print(f"Steps:")
  for item in parsed.steps:
      print(f"- {item}")
  print(f"Preparation time: {parsed.preparation_time}")
  print(f"Servings: {parsed.servings}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';
  import { z } from 'zod';
  import { zodResponseFormat } from 'writer-sdk/helpers/zod';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  async function main() {
    // Define a schema for structured output
    const recipeSchema = z.object({
      recipeName: z.string(),
      ingredients: z.array(
        z.object({
          name: z.string(),
          amount: z.string(),
        }),
      ),
      steps: z.array(z.string()),
      preparationTime: z.string(),
      servings: z.number(),
    });

    const completion = await client.chat.parse({
      model: 'palmyra-x5',
      messages: [
        { role: 'system', content: 'You are a helpful cooking assistant.' },
        { role: 'user', content: 'Give me a recipe for a simple pasta dish.' },
      ],
      response_format: zodResponseFormat(recipeSchema, 'recipe'),
      temperature: 0.7,
    });

    // Access the parsed structured data
    const message = completion.choices[0]?.message;
    if (message?.parsed) {
      console.log('Recipe name:', message.parsed.recipeName);
      console.log('Ingredients:');
      message.parsed.ingredients.forEach((ing) => {
        console.log(`- ${ing.amount} ${ing.name}`);
      });
      console.log('\nSteps:');
      message.parsed.steps.forEach((step, index) => {
        console.log(`${index + 1}. ${step}`);
      });
      console.log(`\nPreparation time: ${message.parsed.preparationTime}`);
      console.log(`Servings: ${message.parsed.servings}`);
    } else {
      console.log('Raw content:', message?.content);
    }
  }

  main()
  ```
</CodeGroup>


# Column test
Source: https://dev.writer.com/home/temp_component



## Fields

<DynamicImage type={component.type} />

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Type</th>
      <th>Description</th>
      <th>Options</th>
    </tr>
  </thead>
</table>

## Low code usage

This component can be declared directly in Python, using [backend-driven UI](/framework/backend-driven-ui).

```python theme={null}
ui.Column(content={
        "title": "", # str 
        "width": "1", # Union[float, str] 
        "isSticky": "no", # str [yes, no]
        "isCollapsible": "no", # str [yes, no]
        "startCollapsed": "no", # str [yes, no]
        "separatorColor": "", # str 
        "contentPadding": "", # str 
        "contentHAlign": "", # str 
        "contentVAlign": "", # str 
        "cssClasses": "", # str 
    }
)
```


# Generate text from a prompt
Source: https://dev.writer.com/home/text-generation

Generate text using the text generation endpoint. Compare text generation vs chat completion and see API examples.

You can use the [text generation endpoint](/api-reference/completion-api/text-generation) to generate text with an LLM.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Text generation vs. chat completion

The text generation endpoint is appropriate when you need to generate a single text response based on a given prompt, or when you want to ask a specific LLM a question.

The [chat completion endpoint](/home/chat-completion) can generate single messages, or create more complex conversations between a user and a general-purpose LLM. Additionally, the chat completion endpoint offers [tool calling](/home/chat-completion), which you can use to access other LLMs, Knowledge Graphs, and custom functions.

## Endpoint overview

**URL:** `POST https://api.writer.com/v1/completions`

<Warning>
  Using the `/completions` endpoint results in charges for **model usage**. See the [pricing page](/home/pricing) for more information.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/completions' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
    "model": "palmyra-x5",
    "prompt": "Tell me a story",
    "max_tokens": 1000,
    "temperature": 0.7,
    "stream": true
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  text_generation = client.completions.create(
    model="palmyra-x5",
    prompt="Tell me a story",
    max_tokens=1000,
    temperature=0.7,
    stream=True
  )

  for chunk in text_generation:
      print(chunk.value, end="", flush=True)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const text_generation = await client.completions.create({
    model: 'palmyra-x5',
    prompt: 'Tell me a story',
    max_tokens: 1000,
    temperature: 0.7,
    stream: true 
  });

  for await (const chunk of text_generation) {
      process.stdout.write(chunk.value);
  }
  ```
</CodeGroup>

### Request body

Below are the required and commonly used optional parameters for the text generation endpoint.

| Parameter     | Type    | Description                                                                                                                                                                      |
| ------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`       | string  | **Required**. The [ID of the model](/home/models) to use for text generation.                                                                                                    |
| `prompt`      | string  | **Required**. The prompt to generate text from.                                                                                                                                  |
| `max_tokens`  | int     | The maximum number of tokens to generate for the response. Defaults to `100`.                                                                                                    |
| `temperature` | float   | Temperature influences the randomness in generated text. Defaults to `1`. Increase the value for more creative responses, and decrease the value for more predictable responses. |
| `stream`      | Boolean | A Boolean value that indicates whether to stream the response. Defaults to `false`.                                                                                              |

See the full list of available parameters in the [text generation endpoint reference](/api-reference/completion-api/text-generation).

### Response parameters

#### Non-streaming response

If you set the `stream` parameter to `false`, the response is a single JSON object with the following parameters:

| Parameter              | Type   | Description                                                                                                                            |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `model`                | string | The ID of the model used to generate the response.                                                                                     |
| `choices`              | array  | An array of choices objects.                                                                                                           |
| `choices[0].text`      | string | The generated text.                                                                                                                    |
| `choices[0].log_probs` | object | The [log probabilities](/api-reference/completion-api/text-generation#response-choices-log-probs) of the tokens in the generated text. |

```json theme={null}
{
  "choices": [
    {
      "text": "Camping Gear: The Ultimate Guide\n\nCamping is a great way to get outdoors and enjoy nature",
      "log_probs": null
    }
  ],
  "model": "palmyra-x5"
}
```

#### Streaming response

If you set the `stream` parameter to `true`, the response is delivered as [server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) with the following parameters:

| Parameter | Description               |
| --------- | ------------------------- |
| `value`   | The content of the chunk. |

```json theme={null}
data: {"value":"Camping Gear: The Ultimate Guide\n\nCamping is a great way to get outdoors and enjoy nature"}
```

## Generate streaming and non-streaming responses

The examples below generate a single message from the `palmyra-x5` model, using the prompt "How can I treat a cold?"

### Streaming response

The text generation endpoint supports streaming responses. The response comes in chunks until the entire response finishes.

Streaming responses are useful when you want to display the generated text in real-time, or when you want to stream the response to a client, rather than waiting for the entire response to finish.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/completions' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
    "model": "palmyra-x5",
    "prompt": "How can I treat a cold?",
    "stream": true
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  text_generation = client.completions.create(
    model="palmyra-x5",
    prompt="How can I treat a cold?",
    stream=True
  )

  for chunk in text_generation:
      print(chunk.value, end="", flush=True)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const text_generation = await client.completions.create({
    model: 'palmyra-x5',
    prompt: 'How can I treat a cold?',
    stream: true 
  });

  for await (const chunk of text_generation) {
      process.stdout.write(chunk.value);
  }
  ```
</CodeGroup>

### Non-streaming response

For non-streaming responses, the response returns as a single JSON object after the entire response is complete. The text is in the `choices[0].text` field.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/completions' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $WRITER_API_KEY" \
  --data '{
    "model": "palmyra-x5",
    "prompt": "How can I treat a cold?",
    "stream": false
  }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  text_generation = client.completions.create(
    model="palmyra-x5",
    prompt="How can I treat a cold?",
    stream=False
  )

  print(text_generation.choices[0].text)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from 'writer-sdk';

  // Initialize the client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const text_generation = await client.completions.create({
    model: 'palmyra-x5',
    prompt: 'How can I treat a cold?',
    stream: false
  });

  console.log(text_generation.choices[0].text);
  ```
</CodeGroup>

## Next steps

Now that you've generated text, try out the following:

* Create a chat with an AI assistant using the [chat completion endpoint](/home/chat-completion)
* Learn more about the [tool calling](/home/chat-completion#tool-calling) feature of the chat completion endpoint


# Understand tokens
Source: https://dev.writer.com/home/tokens



Tokens are the atomic units that language models use to process text. Language models understand tokens rather than characters or bytes. Each token represents a single unit of meaning, like a word or a group of words.

The Writer API processes all text input and output in terms of tokens. See the maximum number of tokens for each model in the [models overview](/home/models) page.

## Tokenization

Tokenization is the process of breaking input text down into smaller units. Depending on the tokenization method the model uses, a single word can be represented by one token or may be broken down into multiple sub-word tokens.

Here's how different text types typically tokenize:

```python theme={null}
"hello world"           → ["hello", " world"]           # 2 tokens
"API"                   → ["API"]                       # 1 token
"tokenization"          → ["token", "ization"]          # 2 tokens
"www.writer.com"        → ["www", "writer", "com"]      # 3 tokens
```

## Input and output token usage

Every API call consumes tokens for both the prompt you send and the response generated:

```python theme={null}
# Example API call
response = client.chat.completions.create(
  model="palmyra-x5",
  messages=[
    {"role": "user", "content": "Write a product description for a cozy sweater"},  # ~8 tokens
  ],
  max_tokens=150,  # Maximum tokens to include in the response
)

# Total token usage = prompt tokens + response tokens
```

## Inspect token usage

You can inspect the [`usage`](/api-reference/completion-api/chat-completion#response-usage) information from the API response to see the token usage for each response.

<Info>
  The prompt tokens reported in the response include the tokens used for the
  model's system prompt.
</Info>

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.chat.chat(
    model="palmyra-x5",
    messages=[{"role": "user", "content": "Write a product description for a cozy sweater"}],
    max_tokens=150,
  )

  # Log token usage to understand patterns
  print(f"Prompt tokens: {response.usage.prompt_tokens}")
  print(f"Response tokens: {response.usage.completion_tokens}")
  print(f"Total tokens: {response.usage.total_tokens}")
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.chat.chat({
    model: "palmyra-x5",
    messages: [
      { role: "user", content: "Write a product description for a cozy sweater" },
    ],
    maxTokens: 150,
  });

  // Log token usage to understand patterns
  console.log(`Prompt tokens: ${response.usage.prompt_tokens}`);
  console.log(`Response tokens: ${response.usage.completion_tokens}`);
  console.log(`Total tokens: ${response.usage.total_tokens}`);
  ```
</CodeGroup>

You can view your organization's overall token usage and spend in the [Consumption dashboard](/home/usage-and-spend).


# Call a custom function in a chat
Source: https://dev.writer.com/home/tool-calling

Extend LLM capabilities with tool calling. Execute custom functions, query databases, fetch real-time data, and integrate results into chat responses.

This guide helps you understand how to use tool calling, sometimes known as function calling, with chat completions.

Tool calling allows you to extend the capabilities of [chats with LLMs](/home/chat-completion) by enabling the LLM to call custom functions, or tools.

Your custom tools can perform a wide range of tasks, such as querying databases, fetching real-time data from APIs, processing data, or executing business logic. You can then integrate the result of these tool calls back into the model's output.

Tool calling is available for Palmyra X4 and later models, as well as supported external models.

<Tip>
  This guide discusses calling custom functions as tools. Writer also offers prebuilt tools that models can execute remotely:

  * [Ask questions to a Knowledge Graph in a chat](/home/kg-chat)
  * [Delegate questions to another model](/home/model-delegation)
  * [Search the web](/home/web-search-tool)
  * [Translate text](/home/translation-tool)
</Tip>

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Overview

To use tool calling, follow these steps:

1. Define your functions in code
2. Pass the functions to the model in a chat completion request
3. Append the assistant's response (containing tool calls) to the message history
4. Check to see which functions the model wants to invoke and run the corresponding functions
5. Append the tool results to the message history
6. Pass the updated messages back to the model to get the final response
7. Append the final response to maintain complete conversation history

<CardGroup>
  <Card title="Tool calling overview">
    <img alt="A flowchart showing how tool calling works: The model receives a prompt, decides to call a tool, executes the tool, and incorporates the result into its response" />
  </Card>

  <Card title="Example: Calculate the mean of a list of numbers">
    <img alt="A flowchart demonstrating tool calling with a specific example: The model receives numbers, calls a calculate_mean function, gets the result, and provides the final answer" />
  </Card>

  <Card title="Example: Calculate the mean and standard deviation of a list of numbers">
    <img alt="Diagram of multi-tool calling to calculate the mean and standard deviation of a list of numbers" />
  </Card>
</CardGroup>

Continue reading to learn more about each step.

## Define your custom functions

First, define the custom functions in your code. Typical use cases for tool calling include calling an API, performing mathematical calculations, or running complex business logic. You can define these functions in your code as you would any other function.

Here's an example of a function to calculate the mean of a list of numbers.

<CodeGroup>
  ```python Python theme={null}
  def calculate_mean(numbers: list) -> float:
      return sum(numbers) / len(numbers)
  ```

  ```js JavaScript theme={null}
  function calculateMean(numbers) {
    if (numbers.length === 0) {
      throw new Error("Cannot calculate mean of an empty array");
    }
    return numbers.reduce((sum, num) => sum + num, 0) / numbers.length;
  }
  ```
</CodeGroup>

## Describe functions as tools

After you've defined your functions, create a `tools` array to pass to the model.

The `tools` array describes your functions as tools available to the model. You describe tools in the form of a [JSON schema](https://json-schema.org/). Each tool should include a `type` of `function` and a `function` object that includes a `name`, `description`, and a  dictionary of `parameters`.

### Tool structure

The `tools` array contains an object with the following parameters:

| Parameter                        | Type   | Description                                                                                                                      |
| -------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `type`                           | string | The type of tool, which is `function` for a custom function                                                                      |
| `function`                       | object | An object containing the tool's description and parameter definitions                                                            |
| `function.name`                  | string | The name of the tool                                                                                                             |
| `function.description`           | string | A description of what the tool does and when the model should use it                                                             |
| `function.parameters`            | object | An object containing the tool's input parameters                                                                                 |
| `function.parameters.type`       | string | The type of the parameter, which is `object` for a JSON schema                                                                   |
| `function.parameters.properties` | object | An object containing the tool's parameters in the form of a [JSON schema](https://json-schema.org/). See below for more details. |
| `function.parameters.required`   | array  | An array of the tool's required parameters                                                                                       |

See the full [tools object schema](/api-reference/completion-api/chat-completion#body-tools) for more details.

The `function.parameters.properties` object contains the tool's parameter definitions as a [JSON schema](https://json-schema.org/). The object's keys should be the names of the parameters, and the values should be objects containing the parameter's type and description.

When the model decides you should use the tool to answer the user's question, it returns the parameters that you should use when calling the function you've defined.

### Example tool array

Here's an example of a `tools` array for the `calculate_mean` function:

<CodeGroup>
  ```python Python theme={null}
  tools = [
      {
          "type": "function",
          "function": {
              "name": "calculate_mean",
              "description": "A function that calculates the mean (average) of a list of numbers. Any user request asking for the mean of a list of numbers should use this tool.",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "numbers": {
                          "type": "array",
                          "items": {"type": "number"},
                          "description": "List of numbers"
                      }
                  },
                  "required": ["numbers"]
              }
          }
      }
  ]
  ```

  ```js JavaScript theme={null}
  const tools = [
      {
          type: "function",
          function: {
              name: "calculate_mean",
              description: "A function that calculates the mean (average) of a list of numbers. Any user request asking for the mean of a list of numbers should use this tool.",
              parameters: {
                  type: "object",
                  properties: {
                      numbers: {
                          type: "array",
                          items: { type: "number" },
                          description: "List of numbers",
                      },
                  },
              required: ["numbers"],
              },
          },
      },
  ];
  ```
</CodeGroup>

<Note>
  To help the model understand when to use the tool, follow these best practices for the `function.description` parameter:

  * Indicate that the tool is a function that invokes a no-code agent
  * Specify the function's purpose and capabilities
  * Describe when the tool should be used

  An example description for a tool that invokes a function to calculate the mean of a list of numbers:

  > "A function that calculates the mean of a list of numbers. Any user request asking for the mean of a list of numbers should use this tool."
</Note>

## Pass tools to the model

Once the tools array is complete, pass it to the [chat completions endpoint](/home/chat-completion) along with the chat messages.

### Tool choice control

The chat completion endpoint has a [`tool_choice parameter`](/api-reference/completion-api/chat-completion#body-tool-choice) that controls how the model decides when to use the tools you've defined.

| Value      | Description                                                        |
| ---------- | ------------------------------------------------------------------ |
| `auto`     | The model decides which tools to use, if any.                      |
| `none`     | The model doesn't use tools and only returns a generated response. |
| `required` | The model must use at least one of the tools you've defined.       |

You can also use a JSON object to force the model to use a specific tool. For example, if you want the model to use the `calculate_mean` tool, you can set `tool_choice` to `{"type": "function", "function": {"name": "calculate_mean"}}`.

In this example, `tool_choice` is `auto`, which means the model decides which tools to use, if any, based on the message and tool descriptions.

<CodeGroup>
  ```python Python theme={null}
  import json
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}]

  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto"
  )
  ```

  ```js JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  let messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}];

  const response = await client.chat.chat({
        model: "palmyra-x5",
        messages: messages,
        tools: tools,
        tool_choice: "auto"
  });
  ```
</CodeGroup>

## Process tool calls

When the model identifies a need to call a tool based on the user's input, it indicates it in the response and includes the necessary parameters to pass when calling the tool. You then execute the tool's function and return the result to the model.

<Tip>
  Proper conversation history management requires appending the assistant's response, tool results, and final response to the message history.
</Tip>

The method for checking for tool calls and executing the tool's function differs depending on whether you're streaming the response or not. Each method is described below.

<Note>
  The examples below demonstrate processing a single tool call. For handling multiple tool calls in one request, see the [multiple tool calls](#multiple-tool-calls) section.
</Note>

<Tabs>
  <Tab title="Streaming">
    ### Streaming

    When using streaming, the tool calls come back in chunks inside of the delta object of the choices array. To process the tool calls:

    1. Stream and collect tool calls from the response chunks
    2. Reconstruct the assistant's response and append it to the message history
    3. Execute functions and append tool results to the message history
    4. Get the final response from the model and append it to the message history

    #### Step 1: Stream and collect tool calls from the response chunks

    First, stream and collect tool calls from the response chunks.

    <CodeGroup>
      ```python Python theme={null}
      streaming_content = ""
      function_calls = []

      for chunk in response:
          choice = chunk.choices[0]

          if choice.delta:
              # Collect tool calls as they stream in
              if choice.delta.tool_calls:
                  for tool_call in choice.delta.tool_calls:
                      if tool_call.id:
                          # Start a new function call
                          function_calls.append({
                              "name": "",
                              "arguments": "",
                              "call_id": tool_call.id
                          })
                      if tool_call.function:
                          # Append to the most recent function call
                          if function_calls:
                              function_calls[-1]["name"] += tool_call.function.name or ""
                              function_calls[-1]["arguments"] += tool_call.function.arguments or ""

              # Collect regular content (for cases where no tools are called)
              elif choice.delta.content:
                  streaming_content += choice.delta.content
      ```

      ```js JavaScript theme={null}
      let streamingContent = "";
      const functionCalls = [];

      for await (const chunk of response) {
          const choice = chunk.choices[0];

          if (choice.delta) {
              if (choice.delta.tool_calls) {
                  for (const toolCall of choice.delta.tool_calls) {
                      if (toolCall.id) {
                          functionCalls.push({
                              name: "",
                              arguments: "",
                              call_id: toolCall.id
                          });
                      }
                      if (toolCall.function) {
                          if (functionCalls.length > 0) {
                              functionCalls[functionCalls.length - 1].name += toolCall.function.name || "";
                              functionCalls[functionCalls.length - 1].arguments += toolCall.function.arguments || "";
                          }
                      }
                  }
              } else if (choice.delta.content) {
                  streamingContent += choice.delta.content;
              }
          }
      }
      ```
    </CodeGroup>

    #### Step 2: Check finish reason and reconstruct the assistant's response

    Check the `finish_reason` to determine if tools were called, then reconstruct and append the assistant's response to the conversation history.

    <CodeGroup>
      ```python Python theme={null}
      # Inside the streaming loop, check for finish_reason
      if choice.finish_reason:
          if choice.finish_reason == "stop":
              # No tools were called, just regular response
              messages.append({"role": "assistant", "content": streaming_content})
              break

          elif choice.finish_reason == "tool_calls":
              # Reconstruct and append assistant message with tool calls
              tool_calls_for_message = []
              for func_call in function_calls:
                  tool_calls_for_message.append({
                      "id": func_call["call_id"],
                      "type": "function",
                      "function": {
                          "name": func_call["name"],
                          "arguments": func_call["arguments"]
                      }
                  })

              assistant_message = {
                  "role": "assistant",
                  "content": None,
                  "tool_calls": tool_calls_for_message
              }
              messages.append(assistant_message)
      ```

      ```js JavaScript theme={null}
      // Inside the streaming loop, check for finish_reason
      if (choice.finish_reason) {
          if (choice.finish_reason === "stop") {
              // No tools were called, just regular response
              messages.push({ role: "assistant", content: streamingContent });
              break;
          } else if (choice.finish_reason === "tool_calls") {
              // Reconstruct and append assistant message with tool calls
              const toolCallsForMessage = [];
              for (const funcCall of functionCalls) {
                  toolCallsForMessage.push({
                      id: funcCall.call_id,
                      type: "function",
                      function: {
                          name: funcCall.name,
                          arguments: funcCall.arguments
                      }
                  });
              }

              const assistantMessage = {
                  role: "assistant",
                  content: null,
                  tool_calls: toolCallsForMessage
              };
              messages.push(assistantMessage);  // Append assistant response
          }
      }
      ```
    </CodeGroup>

    #### Step 3: Execute functions and append tool results

    Execute each function in the `function_calls` list and append the results to the messages array.

    <CodeGroup>
      ```python Python theme={null}
      # Inside the tool_calls finish_reason block
      for function_call in function_calls:
          function_name = function_call["name"]

          if function_name == "calculate_mean":
              try:
                  arguments_dict = json.loads(function_call["arguments"])
                  function_response = calculate_mean(arguments_dict["numbers"])

                  # Append tool result to conversation history
                  messages.append({
                      "role": "tool",
                      "content": str(function_response),
                      "tool_call_id": function_call["call_id"],
                      "name": function_name,
                  })
              except Exception as e:
                  # Handle errors gracefully
                  messages.append({
                      "role": "tool",
                      "content": f"Error: {str(e)}",
                      "tool_call_id": function_call["call_id"],
                      "name": function_name,
                  })
      ```

      ```js JavaScript theme={null}
      // Inside the tool_calls finish_reason block
      for (const functionCall of functionCalls) {
          const functionName = functionCall.name;

          if (functionName === "calculate_mean") {
              try {
                  const argumentsDict = JSON.parse(functionCall.arguments);
                  const functionResponse = calculateMean(argumentsDict.numbers);

                  // Append tool result to conversation history
                  messages.push({
                      role: "tool",
                      content: functionResponse.toString(),
                      tool_call_id: functionCall.call_id,
                      name: functionName,
                  });
              } catch (error) {
                  // Handle errors gracefully
                  messages.push({
                      role: "tool",
                      content: `Error: ${error.message}`,
                      tool_call_id: functionCall.call_id,
                      name: functionName,
                  });
              }
          }
      }
      ```
    </CodeGroup>

    #### Step 4: Get and append the final response

    After appending tool results, get the final response from the model and append it to maintain complete conversation history.

    <CodeGroup>
      ```python Python theme={null}
      # Inside the tool_calls finish_reason block, after processing all functions
      final_response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          stream=True
      )

      final_content = ""
      for chunk in final_response:
          choice = chunk.choices[0]
          if choice.delta and choice.delta.content:
              final_content += choice.delta.content

      # Append final response to conversation history
      messages.append({
          "role": "assistant",
          "content": final_content
      })
      ```

      ```js JavaScript theme={null}
      // Inside the tool_calls finish_reason block, after processing all functions
      const finalResponse = await client.chat.chat({
          model: "palmyra-x5",
          messages: messages,
          stream: true
      });

      let finalContent = "";
      for await (const chunk of finalResponse) {
          const choice = chunk.choices[0];
          if (choice.delta && choice.delta.content) {
              finalContent += choice.delta.content;
              process.stdout.write(choice.delta.content);
          }
      }

      // Append final response to conversation history
      messages.push({
          role: "assistant",
          content: finalContent
      });
      ```
    </CodeGroup>

    #### Complete streaming code example

    <CodeGroup>
      ```python Python [expandable] theme={null}
      import json
      from writerai import Writer

      # Initialize the Writer client. If you don't pass the `apiKey` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      def calculate_mean(numbers: list) -> float:
          return sum(numbers) / len(numbers)

      tools = [
          {
              "type": "function",
              "function": {
                  "name": "calculate_mean",
                  "description": "Calculate the mean (average) of a list of numbers.",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "numbers": {
                              "type": "array",
                              "items": {"type": "number"},
                              "description": "List of numbers"
                          }
                      },
                      "required": ["numbers"]
                  }
              }
          }
      ]

      messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}]

      # Step 1: Initial request with tools
      response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          tools=tools,
          tool_choice="auto",
          stream=True
      )

      # Step 2: Process streaming response to collect tool calls
      streaming_content = ""
      function_calls = []

      for chunk in response:
          choice = chunk.choices[0]

          if choice.delta:
              # Collect tool calls as they stream in
              if choice.delta.tool_calls:
                  for tool_call in choice.delta.tool_calls:
                      if tool_call.id:
                          # Start a new function call
                          function_calls.append({
                              "name": "",
                              "arguments": "",
                              "call_id": tool_call.id
                          })
                      if tool_call.function:
                          # Append to the most recent function call
                          if function_calls:
                              function_calls[-1]["name"] += tool_call.function.name or ""
                              function_calls[-1]["arguments"] += tool_call.function.arguments or ""

              # Collect regular content (for cases where no tools are called)
              elif choice.delta.content:
                  streaming_content += choice.delta.content

          # Check if streaming is complete
          if choice.finish_reason:
              if choice.finish_reason == "stop":
                  # No tools were called, just regular response
                  print(f"Response: {streaming_content}")
                  messages.append({"role": "assistant", "content": streaming_content})
                  break

              elif choice.finish_reason == "tool_calls":
                  # Step 3: Reconstruct and append assistant message with tool calls
                  tool_calls_for_message = []
                  for func_call in function_calls:
                      tool_calls_for_message.append({
                          "id": func_call["call_id"],
                          "type": "function",
                          "function": {
                              "name": func_call["name"],
                              "arguments": func_call["arguments"]
                          }
                      })

                  assistant_message = {
                      "role": "assistant",
                      "content": None,
                      "tool_calls": tool_calls_for_message
                  }
                  messages.append(assistant_message)  # Append assistant response

                  # Step 4: Execute each function and add results to messages
                  for function_call in function_calls:
                      function_name = function_call["name"]

                      if function_name == "calculate_mean":
                          try:
                              arguments_dict = json.loads(function_call["arguments"])
                              function_response = calculate_mean(arguments_dict["numbers"])

                              # Add tool response to messages
                              messages.append({
                                  "role": "tool",
                                  "content": str(function_response),
                                  "tool_call_id": function_call["call_id"],
                                  "name": function_name,
                              })
                          except Exception as e:
                              # Handle errors gracefully
                              messages.append({
                                  "role": "tool",
                                  "content": f"Error: {str(e)}",
                                  "tool_call_id": function_call["call_id"],
                                  "name": function_name,
                              })

                  # Step 5: Get the final response from the model
                  final_response = client.chat.chat(
                      model="palmyra-x5",
                      messages=messages,
                      stream=True
                  )

                  final_content = ""
                  for chunk in final_response:
                      choice = chunk.choices[0]
                      if choice.delta and choice.delta.content:
                          final_content += choice.delta.content
                          print(choice.delta.content, end="", flush=True)

                  # Step 6: Add final response to message history
                  messages.append({
                      "role": "assistant",
                      "content": final_content
                  })
                  break
      ```

      ```js JavaScript [expandable] theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the Writer client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      function calculateMean(numbers) {
          if (numbers.length === 0) {
              throw new Error("Cannot calculate mean of an empty array");
          }
          return numbers.reduce((sum, num) => sum + num, 0) / numbers.length;
      }

      const tools = [
          {
              type: "function",
              function: {
                  name: "calculate_mean",
                  description: "Calculate the mean (average) of a list of numbers.",
                  parameters: {
                      type: "object",
                      properties: {
                          numbers: {
                              type: "array",
                              items: { type: "number" },
                              description: "List of numbers",
                          },
                      },
                      required: ["numbers"],
                  },
              },
          },
      ];

      async function main() {
          let messages = [
              { role: "user", content: "what is the mean of [1,3,5,7,9]?" },
          ];

          // Step 1: Initial request with tools
          const response = await client.chat.chat({
              model: "palmyra-x5",
              messages: messages,
              tools: tools,
              tool_choice: "auto",
              stream: true
          });

          // Step 2: Process streaming response to collect tool calls
          let streamingContent = "";
          const functionCalls = [];

          for await (const chunk of response) {
              const choice = chunk.choices[0];

              if (choice.delta) {
                  // Collect tool calls as they stream in
                  if (choice.delta.tool_calls) {
                      for (const toolCall of choice.delta.tool_calls) {
                          if (toolCall.id) {
                              // Start a new function call
                              functionCalls.push({
                                  name: "",
                                  arguments: "",
                                  call_id: toolCall.id,
                              });
                          }
                          if (toolCall.function) {
                              // Append to the most recent function call
                              if (functionCalls.length > 0) {
                                  functionCalls[functionCalls.length - 1].name += toolCall.function.name || "";
                                  functionCalls[functionCalls.length - 1].arguments += toolCall.function.arguments || "";
                              }
                          }
                      }
                  }
                  // Collect regular content (for cases where no tools are called)
                  else if (choice.delta.content) {
                      streamingContent += choice.delta.content;
                  }
              }

              // Check if streaming is complete
              if (choice.finish_reason) {
                  if (choice.finish_reason === "stop") {
                      // No tools were called, just regular response
                      console.log(`Response: ${streamingContent}`);
                      messages.push({ role: "assistant", content: streamingContent });
                      break;
                  } else if (choice.finish_reason === "tool_calls") {
                      // Step 3: Reconstruct and append assistant message with tool calls
                      const toolCallsForMessage = [];
                      for (const funcCall of functionCalls) {
                          toolCallsForMessage.push({
                              id: funcCall.call_id,
                              type: "function",
                              function: {
                                  name: funcCall.name,
                                  arguments: funcCall.arguments,
                              },
                          });
                      }

                      const assistantMessage = {
                          role: "assistant",
                          content: null,
                          tool_calls: toolCallsForMessage,
                      };
                      messages.push(assistantMessage); // Append assistant response

                      // Step 4: Execute each function and add results to messages
                      for (const functionCall of functionCalls) {
                          const functionName = functionCall.name;

                          if (functionName === "calculate_mean") {
                              try {
                                  const argumentsDict = JSON.parse(functionCall.arguments);
                                  const functionResponse = calculateMean(argumentsDict.numbers);

                                  // Add tool response to messages
                                  messages.push({
                                      role: "tool",
                                      content: functionResponse.toString(),
                                      tool_call_id: functionCall.call_id,
                                      name: functionName,
                                  });
                              } catch (error) {
                                  // Handle errors gracefully
                                  messages.push({
                                      role: "tool",
                                      content: `Error: ${error.message}`,
                                      tool_call_id: functionCall.call_id,
                                      name: functionName,
                                  });
                              }
                          }
                      }

                      // Step 5: Get the final response from the model
                      const finalResponse = await client.chat.chat({
                          model: "palmyra-x5",
                          messages: messages,
                          stream: true
                      });

                      let finalContent = "";
                      for await (const chunk of finalResponse) {
                          const choice = chunk.choices[0];
                          if (choice.delta && choice.delta.content) {
                              finalContent += choice.delta.content;
                              process.stdout.write(choice.delta.content);
                          }
                      }

                      // Step 6: Add final response to message history
                      messages.push({
                          role: "assistant",
                          content: finalContent,
                      });
                      break;
                  }
              }
          }
      }

      main();
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Non-streaming">
    ### Non-streaming

    If you set `stream` to `false`, the tool calls come back in one object inside of the `message` object in the `choices` array. To process the tool calls:

    1. Get the assistant's response and append it to the message history
    2. Check for tool calls and execute the corresponding functions, appending tool results to the message history
    3. Get the final response from the model and append it to the message history

    #### Step 1: Get and append the assistant's response

    First, get the assistant's response message and append it to your conversation history. This is crucial for maintaining context.

    <CodeGroup>
      ```python Python theme={null}
      response_message = response.choices[0].message
      messages.append(response_message)  # Always append the assistant's response
      ```

      ```js JavaScript theme={null}
      const responseMessage = response.choices[0].message;
      messages.push(responseMessage);  // Always append the assistant's response
      ```
    </CodeGroup>

    #### Step 2: Execute functions and append tool results

    Next, check for tool calls and execute the corresponding functions with the arguments provided by the model. Then, append the tool results to the conversation history.

    <CodeGroup>
      ```python Python theme={null}
      tool_calls = response_message.tool_calls
      if tool_calls:
          for tool_call in tool_calls:
              tool_call_id = tool_call.id
              function_name = tool_call.function.name
              function_args = json.loads(tool_call.function.arguments)

              if function_name == "calculate_mean":
                  function_response = calculate_mean(function_args["numbers"])

                  # Append tool result to conversation history
                  messages.append({
                      "role": "tool",
                      "tool_call_id": tool_call_id,
                      "name": function_name,
                      "content": str(function_response),
                  })
      ```

      ```js JavaScript theme={null}
      const toolCalls = responseMessage.tool_calls;
      if (toolCalls && toolCalls.length > 0) {
          for (const toolCall of toolCalls) {
              const toolCallId = toolCall.id;
              const functionName = toolCall.function.name;
              const functionArgs = JSON.parse(toolCall.function.arguments);

              if (functionName === "calculate_mean") {
                  const functionResponse = calculateMean(functionArgs.numbers);

                  // Append tool result to conversation history
                  messages.push({
                      role: "tool",
                      tool_call_id: toolCallId,
                      name: functionName,
                      content: functionResponse.toString(),
                  });
              }
          }
      }
      ```
    </CodeGroup>

    #### Step 3: Get and append the final response

    After appending tool results, get the final response from the model and append it to maintain complete conversation history:

    <CodeGroup>
      ```python Python theme={null}
      final_response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          stream=False
      )

      final_content = final_response.choices[0].message.content
      print(f"Final response: {final_content}")

      # Append final response to conversation history
      messages.append({
          "role": "assistant",
          "content": final_content
      })
      ```

      ```js JavaScript theme={null}
      const finalResponse = await client.chat.chat({
          model: "palmyra-x5",
          messages: messages,
          stream: false
      });

      const finalContent = finalResponse.choices[0].message.content;
      console.log(`Final response: ${finalContent}`);

      // Append final response to conversation history
      messages.push({
          role: "assistant",
          content: finalContent
      });
      ```
    </CodeGroup>

    #### Complete non-streaming code example

    <CodeGroup>
      ```python Python [expandable] theme={null}
      import json
      from writerai import Writer

      # Initialize the Writer client. If you don't pass the `apiKey` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      def calculate_mean(numbers: list) -> float:
          return sum(numbers) / len(numbers)

      tools = [
          {
              "type": "function",
              "function": {
                  "name": "calculate_mean",
                  "description": "Calculate the mean (average) of a list of numbers.",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "numbers": {
                              "type": "array",
                              "items": {"type": "number"},
                              "description": "List of numbers"
                          }
                      },
                      "required": ["numbers"]
                  }
              }
          }
      ]

      messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}]

      # Step 1: Initial request with tools
      response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          tools=tools,
          tool_choice="auto",
          stream=False
      )

      # Step 2: Get and append assistant response
      response_message = response.choices[0].message
      messages.append(response_message)
      tool_calls = response_message.tool_calls

      # Step 3: Process tool calls if any
      if tool_calls:
          for tool_call in tool_calls:
              tool_call_id = tool_call.id
              function_name = tool_call.function.name
              function_args = json.loads(tool_call.function.arguments)

              if function_name == "calculate_mean":
                  function_response = calculate_mean(function_args["numbers"])

                  # Append tool result to conversation history
                  messages.append({
                      "role": "tool",
                      "tool_call_id": tool_call_id,
                      "name": function_name,
                      "content": str(function_response),
                  })

          # Step 4: Get final response
          final_response = client.chat.chat(
              model="palmyra-x5",
              messages=messages,
              stream=False
          )

          final_content = final_response.choices[0].message.content
          print(f"Final response: {final_content}")

          # Step 5: Append final response to conversation history
          messages.append({
              "role": "assistant",
              "content": final_content
          })
      else:
          # No tool calls, just use the original response
          print(f"Response: {response_message.content}")
      ```

      ```js JavaScript [expandable] theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the Writer client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      function calculateMean(numbers) {
          if (numbers.length === 0) {
              throw new Error("Cannot calculate mean of an empty array");
          }
          return numbers.reduce((sum, num) => sum + num, 0) / numbers.length;
      }

      const tools = [
          {
              type: "function",
              function: {
                  name: "calculate_mean",
                  description: "Calculate the mean (average) of a list of numbers.",
                  parameters: {
                      type: "object",
                      properties: {
                          numbers: {
                              type: "array",
                              items: { type: "number" },
                              description: "List of numbers",
                          },
                      },
                      required: ["numbers"],
                  },
              },
          },
      ];

      async function main() {
          let messages = [
              { role: "user", content: "what is the mean of [1,3,5,7,9]?" },
          ];

          // Step 1: Initial request with tools
          const response = await client.chat.chat({
              model: "palmyra-x5",
              messages: messages,
              tools: tools,
              tool_choice: "auto",
              stream: false,
          });

          // Step 2: Get and append assistant response
          const responseMessage = response.choices[0].message;
          messages.push(responseMessage);  // Append assistant response
          const toolCalls = responseMessage.tool_calls;

          // Step 3: Process tool calls if any
          if (toolCalls && toolCalls.length > 0) {
              for (const toolCall of toolCalls) {
                  const toolCallId = toolCall.id;
                  const functionName = toolCall.function.name;
                  const functionArgs = JSON.parse(toolCall.function.arguments);

                  if (functionName === "calculate_mean") {
                      const functionResponse = calculateMean(functionArgs.numbers);

                      // Append tool result to conversation history
                      messages.push({
                          role: "tool",
                          tool_call_id: toolCallId,
                          name: functionName,
                          content: functionResponse.toString(),
                      });
                  }
              }

              // Step 4: Get final response
              const finalResponse = await client.chat.chat({
                  model: "palmyra-x5",
                  messages: messages,
                  stream: false
              });

              const finalContent = finalResponse.choices[0].message.content;
              console.log(`Final response: ${finalContent}`);

              // Step 5: Append final response to conversation history
              messages.push({
                  role: "assistant",
                  content: finalContent
              });
          } else {
              // No tool calls, just use the original response
              console.log(`Response: ${responseMessage.content}`);
          }
      }

      main();
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Multiple tool calls

When the model uses multiple tools in a single request, you'll receive multiple tool calls in the response. This section shows you how to handle multiple tool calls for both streaming and non-streaming approaches.

<Note>
  **Non-streaming**: The processing logic for multiple tool calls is identical to single tool calls - the same code handles both scenarios. The only difference is that the loop for checking the response for tool calling processes multiple tool calls instead of one.

  **Streaming**: Multiple tool calls require different logic because tool calls stream in chunks. You need to collect and reconstruct multiple tool calls from streaming chunks before processing them.
</Note>

### Define multiple functions

First, define the functions in your code.

<CodeGroup>
  ```python Python theme={null}
  import math

  def calculate_mean(numbers: list) -> float:
      if not numbers:
          raise ValueError("Cannot calculate mean of an empty list")
      return sum(numbers) / len(numbers)

  def calculate_standard_deviation(numbers: list) -> float:
      if len(numbers) < 2:
          raise ValueError("Cannot calculate standard deviation with fewer than 2 numbers")
      mean = calculate_mean(numbers)
      variance = sum((x - mean) ** 2 for x in numbers) / len(numbers)
      return math.sqrt(variance)
  ```

  ```js JavaScript theme={null}
  function calculateMean(numbers) {
      if (numbers.length === 0) {
          throw new Error("Cannot calculate mean of an empty array");
      }
      return numbers.reduce((sum, num) => sum + num, 0) / numbers.length;
  }

  function calculateStandardDeviation(numbers) {
      if (numbers.length < 2) {
          throw new Error("Cannot calculate standard deviation with fewer than 2 numbers");
      }
      const mean = calculateMean(numbers);
      const variance = numbers.reduce((sum, num) => sum + Math.pow(num - mean, 2), 0) / numbers.length;
      return Math.sqrt(variance);
  }
  ```
</CodeGroup>

### Define tools array with multiple functions

Create a tools array that includes both functions:

<Note>
  For more information about describing functions as tools, see [describe functions as tools section](#describe-functions-as-tools)
</Note>

<CodeGroup>
  ```python Python theme={null}
  tools = [
      {
          "type": "function",
          "function": {
              "name": "calculate_mean",
              "description": "Calculate the mean (average) of a list of numbers. Use this when asked for the average or mean of numbers.",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "numbers": {
                          "type": "array",
                          "items": {"type": "number"},
                          "description": "List of numbers to calculate the mean for"
                      }
                  },
                  "required": ["numbers"]
              }
          }
      },
      {
          "type": "function",
          "function": {
              "name": "calculate_standard_deviation",
              "description": "Calculate the population standard deviation (N) of a list of numbers. Use this when asked for standard deviation, variance, or statistical spread of numbers.",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "numbers": {
                          "type": "array",
                          "items": {"type": "number"},
                          "description": "List of numbers to calculate the standard deviation for"
                      }
                  },
                  "required": ["numbers"]
              }
          }
      }
  ]
  ```

  ```js JavaScript theme={null}
  const tools = [
      {
          type: "function",
          function: {
              name: "calculate_mean",
              description: "Calculate the mean (average) of a list of numbers. Use this when asked for the average or mean of numbers.",
              parameters: {
                  type: "object",
                  properties: {
                      numbers: {
                          type: "array",
                          items: { type: "number" },
                          description: "List of numbers to calculate the mean for"
                      }
                  },
                  required: ["numbers"]
              }
          }
      },
      {
          type: "function",
          function: {
              name: "calculate_standard_deviation",
              description: "Calculate the population standard deviation (N) of a list of numbers. Use this when asked for standard deviation, variance, or statistical spread of numbers.",
              parameters: {
                  type: "object",
                  properties: {
                      numbers: {
                          type: "array",
                          items: { type: "number" },
                          description: "List of numbers to calculate the standard deviation for"
                      }
                  },
                  required: ["numbers"]
              }
          }
      }
  ];
  ```
</CodeGroup>

## Process multiple tool calls

When the model identifies a need to call multiple tools based on the user’s input, you'll receive multiple tool calls in the response. The processing differs between streaming and non-streaming approaches.

<Tabs>
  <Tab title="Streaming">
    ### Streaming

    When using streaming with multiple tool calls, the tool calls come back in chunks inside of the delta object of the choices array. To process multiple tool calls:

    1. Stream and collect multiple tool calls from the response chunks
    2. Reconstruct the assistant's response with all tool calls and append it to the message history
    3. Execute all functions and append tool results to the message history
    4. Get the final response from the model and append it to the message history

    This requires different logic from single tool calls because you need to collect and reconstruct multiple tool calls from streaming chunks.

    #### Step 1: Stream and collect multiple tool calls from the response chunks

    First, stream and collect multiple tool calls from the response chunks. This is where multiple tool calls differ from single tool calls:

    <CodeGroup>
      ```python Python theme={null}
      streaming_content = ""
      function_calls = []

      for chunk in response:
          choice = chunk.choices[0]

          if choice.delta:
              # Collect tool calls as they stream in
              if choice.delta.tool_calls:
                  for tool_call in choice.delta.tool_calls:
                      if tool_call.id:
                          # Start a new function call
                          function_calls.append({
                              "name": "",
                              "arguments": "",
                              "call_id": tool_call.id
                          })
                      if tool_call.function:
                          # Append to the most recent function call
                          if function_calls:
                              function_calls[-1]["name"] += tool_call.function.name or ""
                              function_calls[-1]["arguments"] += tool_call.function.arguments or ""

              # Collect regular content (for cases where no tools are called)
              elif choice.delta.content:
                  streaming_content += choice.delta.content

          # Check if streaming is complete
          if choice.finish_reason:
              if choice.finish_reason == "stop":
                  # No tools were called, just regular response
                  messages.append({"role": "assistant", "content": streaming_content})
                  break

              elif choice.finish_reason == "tool_calls":
                  print(f"Tool calls collected: {len(function_calls)}")
                  break
      ```

      ```js JavaScript theme={null}
      let streamingContent = "";
      const functionCalls = [];

      for await (const chunk of response) {
          const choice = chunk.choices[0];

          if (choice.delta) {
              // Collect tool calls as they stream in
              if (choice.delta.tool_calls) {
                  for (const toolCall of choice.delta.tool_calls) {
                      if (toolCall.id) {
                          // Start a new function call
                          functionCalls.push({
                              name: "",
                              arguments: "",
                              call_id: toolCall.id
                          });
                      }
                      if (toolCall.function) {
                          // Append to the most recent function call
                          if (functionCalls.length > 0) {
                              functionCalls[functionCalls.length - 1].name += toolCall.function.name || "";
                              functionCalls[functionCalls.length - 1].arguments += toolCall.function.arguments || "";
                          }
                      }
                  }
              }
              // Collect regular content (for cases where no tools are called)
              else if (choice.delta.content) {
                  streamingContent += choice.delta.content;
              }
          }

          // Check if streaming is complete
          if (choice.finish_reason) {
              if (choice.finish_reason === "stop") {
                  // No tools were called, just regular response
                  messages.push({ role: "assistant", content: streamingContent });
                  break;
              } else if (choice.finish_reason === "tool_calls") {
                  console.log(`Tool calls collected: ${functionCalls.length}`);
                  break;
              }
          }
      }
      ```
    </CodeGroup>

    #### Step 2: Reconstruct and append assistant message with multiple tool calls

    Reconstruct the assistant message with all collected tool calls:

    <CodeGroup>
      ```python Python theme={null}
      # Step 3: Reconstruct and append assistant message with multiple tool calls
      tool_calls_for_message = []
      for func_call in function_calls:
          tool_calls_for_message.append({
              "id": func_call["call_id"],
              "type": "function",
              "function": {
                  "name": func_call["name"],
                  "arguments": func_call["arguments"]
              }
          })

      assistant_message = {
          "role": "assistant",
          "content": None,
          "tool_calls": tool_calls_for_message
      }
      messages.append(assistant_message)
      ```

      ```js JavaScript theme={null}
      // Step 3: Reconstruct and append assistant message with multiple tool calls
      const toolCallsForMessage = [];
      for (const funcCall of functionCalls) {
          toolCallsForMessage.push({
              id: funcCall.call_id,
              type: "function",
              function: {
                  name: funcCall.name,
                  arguments: funcCall.arguments
              }
          });
      }

      const assistantMessage = {
          role: "assistant",
          content: null,
          tool_calls: toolCallsForMessage
      };
      messages.push(assistantMessage);
      ```
    </CodeGroup>

    #### Step 3: Execute all functions and append results

    Execute each function and append the results to the conversation history:

    <CodeGroup>
      ```python Python theme={null}
      # Step 4: Execute all functions and append results
      for function_call in function_calls:
          function_name = function_call["name"]

          print(f"Processing tool call: {function_name}")

          if function_name == "calculate_mean":
              try:
                  arguments_dict = json.loads(function_call["arguments"])
                  function_response = calculate_mean(arguments_dict["numbers"])
              except Exception as e:
                  function_response = f"Error: {str(e)}"
          elif function_name == "calculate_standard_deviation":
              try:
                  arguments_dict = json.loads(function_call["arguments"])
                  function_response = calculate_standard_deviation(arguments_dict["numbers"])
              except Exception as e:
                  function_response = f"Error: {str(e)}"
          else:
              function_response = f"Unknown function: {function_name}"

          print(f"Function response: {function_response}")

          # Append tool result to conversation history
          messages.append({
              "role": "tool",
              "content": str(function_response),
              "tool_call_id": function_call["call_id"],
              "name": function_name,
          })
      ```

      ```js JavaScript theme={null}
      // Step 4: Execute all functions and append results
      for (const functionCall of functionCalls) {
          const functionName = functionCall.name;
          let functionResponse;

          console.log(`Processing tool call: ${functionName}`);

          if (functionName === "calculate_mean") {
              try {
                  const argumentsDict = JSON.parse(functionCall.arguments);
                  functionResponse = calculateMean(argumentsDict.numbers);
              } catch (error) {
                  functionResponse = `Error: ${error.message}`;
              }
          } else if (functionName === "calculate_standard_deviation") {
              try {
                  const argumentsDict = JSON.parse(functionCall.arguments);
                  functionResponse = calculateStandardDeviation(argumentsDict.numbers);
              } catch (error) {
                  functionResponse = `Error: ${error.message}`;
              }
          } else {
              functionResponse = `Unknown function: ${functionName}`;
          }

          console.log(`Function response: ${functionResponse}`);

          // Append tool result to conversation history
          messages.push({
              role: "tool",
              content: functionResponse.toString(),
              tool_call_id: functionCall.call_id,
              name: functionName,
          });
      }
      ```
    </CodeGroup>

    #### Step 4: Get the final response

    Get the final response from the model after all tool calls are processed:

    <CodeGroup>
      ```python Python theme={null}
      # Step 5: Get the final response
      final_response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          stream=True
      )

      final_content = ""
      for chunk in final_response:
          choice = chunk.choices[0]
          if choice.delta and choice.delta.content:
              final_content += choice.delta.content

      print(f"Final response: {final_content}")
      ```

      ```js JavaScript theme={null}
      // Step 5: Get the final response
      const finalResponse = await client.chat.chat({
          model: "palmyra-x5",
          messages: messages,
          stream: true
      });

      let finalContent = "";
      for await (const chunk of finalResponse) {
          const choice = chunk.choices[0];
          if (choice.delta && choice.delta.content) {
              finalContent += choice.delta.content;
          }
      }

      console.log(`Final response: ${finalContent}`);
      ```
    </CodeGroup>

    #### Step 5: Append final response to conversation history

    Append the final response to maintain complete conversation history:

    <CodeGroup>
      ```python Python theme={null}
      # Step 5: Append final response to conversation history
      messages.append({
          "role": "assistant",
          "content": final_content
      })
      ```

      ```js JavaScript theme={null}
      // Step 5: Append final response to conversation history
      messages.push({
          role: "assistant",
          content: finalContent
      });
      ```
    </CodeGroup>

    #### Complete streaming code example

    <CodeGroup>
      ```python Python [expandable] theme={null}
      import json
      import math
      from writerai import Writer

      # Initialize the Writer client. If you don't pass the `apiKey` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      def calculate_mean(numbers: list) -> float:
          if not numbers:
              raise ValueError("Cannot calculate mean of an empty list")
          return sum(numbers) / len(numbers)

      def calculate_standard_deviation(numbers: list) -> float:
          if len(numbers) < 2:
              raise ValueError("Cannot calculate standard deviation with fewer than 2 numbers")
          mean = calculate_mean(numbers)
          variance = sum((x - mean) ** 2 for x in numbers) / len(numbers)
          return math.sqrt(variance)

      tools = [
          {
              "type": "function",
              "function": {
                  "name": "calculate_mean",
                  "description": "Calculate the mean (average) of a list of numbers. Use this when asked for the average or mean of numbers.",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "numbers": {
                              "type": "array",
                              "items": {"type": "number"},
                              "description": "List of numbers to calculate the mean for"
                          }
                      },
                      "required": ["numbers"]
                  }
              }
          },
          {
              "type": "function",
              "function": {
                  "name": "calculate_standard_deviation",
                  "description": "Calculate the standard deviation of a list of numbers. Use this when asked for standard deviation, variance, or statistical spread of numbers.",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "numbers": {
                              "type": "array",
                              "items": {"type": "number"},
                              "description": "List of numbers to calculate the standard deviation for"
                          }
                      },
                      "required": ["numbers"]
                  }
              }
          }
      ]

      messages = [{"role": "user", "content": "What are the mean and standard deviation of [10, 20, 30, 40, 50]?"}]

      # Step 1: Initial request with tools
      response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          tools=tools,
          tool_choice="auto",
          stream=True
      )

      # Step 2: Process streaming response to collect multiple tool calls
      streaming_content = ""
      function_calls = []

      for chunk in response:
          choice = chunk.choices[0]

          if choice.delta:
              # Collect tool calls as they stream in
              if choice.delta.tool_calls:
                  for tool_call in choice.delta.tool_calls:
                      if tool_call.id:
                          # Start a new function call
                          function_calls.append({
                              "name": "",
                              "arguments": "",
                              "call_id": tool_call.id
                          })
                      if tool_call.function:
                          # Append to the most recent function call
                          if function_calls:
                              function_calls[-1]["name"] += tool_call.function.name or ""
                              function_calls[-1]["arguments"] += tool_call.function.arguments or ""

              # Collect regular content (for cases where no tools are called)
              elif choice.delta.content:
                  streaming_content += choice.delta.content

          # Check if streaming is complete
          if choice.finish_reason:
              if choice.finish_reason == "stop":
                  # No tools were called, just regular response
                  print(f"Response: {streaming_content}")
                  messages.append({"role": "assistant", "content": streaming_content})
                  break

              elif choice.finish_reason == "tool_calls":
                  print(f"Tool calls collected: {len(function_calls)}")
                  break

      # Step 3: Reconstruct and append assistant message with multiple tool calls
      tool_calls_for_message = []
      for func_call in function_calls:
          tool_calls_for_message.append({
              "id": func_call["call_id"],
              "type": "function",
              "function": {
                  "name": func_call["name"],
                  "arguments": func_call["arguments"]
              }
          })

      assistant_message = {
          "role": "assistant",
          "content": None,
          "tool_calls": tool_calls_for_message
      }
      messages.append(assistant_message)

      # Step 4: Execute all functions and append results
      for function_call in function_calls:
          function_name = function_call["name"]

          print(f"Processing tool call: {function_name}")

          if function_name == "calculate_mean":
              try:
                  arguments_dict = json.loads(function_call["arguments"])
                  function_response = calculate_mean(arguments_dict["numbers"])
              except Exception as e:
                  function_response = f"Error: {str(e)}"
          elif function_name == "calculate_standard_deviation":
              try:
                  arguments_dict = json.loads(function_call["arguments"])
                  function_response = calculate_standard_deviation(arguments_dict["numbers"])
              except Exception as e:
                  function_response = f"Error: {str(e)}"
          else:
              function_response = f"Unknown function: {function_name}"

          print(f"Function response: {function_response}")

          # Append tool result to conversation history
          messages.append({
              "role": "tool",
              "content": str(function_response),
              "tool_call_id": function_call["call_id"],
              "name": function_name,
          })

      # Step 5: Get the final response
      final_response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          stream=True
      )

      final_content = ""
      for chunk in final_response:
          choice = chunk.choices[0]
          if choice.delta and choice.delta.content:
              final_content += choice.delta.content
              print(choice.delta.content, end="", flush=True)

      print(f"\nFinal response: {final_content}")

      # Step 6: Append final response to conversation history
      messages.append({
          "role": "assistant",
          "content": final_content
      })
      ```

      ```js JavaScript [expandable] theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the Writer client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      function calculateMean(numbers) {
          if (numbers.length === 0) {
              throw new Error("Cannot calculate mean of an empty array");
          }
          return numbers.reduce((sum, num) => sum + num, 0) / numbers.length;
      }

      function calculateStandardDeviation(numbers) {
          if (numbers.length < 2) {
              throw new Error("Cannot calculate standard deviation with fewer than 2 numbers");
          }
          const mean = calculateMean(numbers);
          const variance = numbers.reduce((sum, num) => sum + Math.pow(num - mean, 2), 0) / numbers.length;
          return Math.sqrt(variance);
      }

      const tools = [
          {
              type: "function",
              function: {
                  name: "calculate_mean",
                  description: "Calculate the mean (average) of a list of numbers. Use this when asked for the average or mean of numbers.",
                  parameters: {
                      type: "object",
                      properties: {
                          numbers: {
                              type: "array",
                              items: { type: "number" },
                              description: "List of numbers to calculate the mean for"
                          }
                      },
                      required: ["numbers"]
                  }
              }
          },
          {
              type: "function",
              function: {
                  name: "calculate_standard_deviation",
                  description: "Calculate the standard deviation of a list of numbers. Use this when asked for standard deviation, variance, or statistical spread of numbers.",
                  parameters: {
                      type: "object",
                      properties: {
                          numbers: {
                              type: "array",
                              items: { type: "number" },
                              description: "List of numbers to calculate the standard deviation for"
                          }
                      },
                      required: ["numbers"]
                  }
              }
          }
      ];

      async function main() {
          let messages = [{ role: "user", content: "What are the mean and standard deviation of [10, 20, 30, 40, 50]?" }];

          // Step 1: Initial request with tools
          const response = await client.chat.chat({
              model: "palmyra-x5",
              messages: messages,
              tools: tools,
              tool_choice: "auto",
              stream: true
          });

          // Step 2: Process streaming response to collect multiple tool calls
          let streamingContent = "";
          const functionCalls = [];

          for await (const chunk of response) {
              const choice = chunk.choices[0];

              if (choice.delta) {
                  // Collect tool calls as they stream in
                  if (choice.delta.tool_calls) {
                      for (const toolCall of choice.delta.tool_calls) {
                          if (toolCall.id) {
                              // Start a new function call
                              functionCalls.push({
                                  name: "",
                                  arguments: "",
                                  call_id: toolCall.id
                              });
                          }
                          if (toolCall.function) {
                              // Append to the most recent function call
                              if (functionCalls.length > 0) {
                                  functionCalls[functionCalls.length - 1].name += toolCall.function.name || "";
                                  functionCalls[functionCalls.length - 1].arguments += toolCall.function.arguments || "";
                              }
                          }
                      }
                  }
                  // Collect regular content (for cases where no tools are called)
                  else if (choice.delta.content) {
                      streamingContent += choice.delta.content;
                  }
              }

              // Check if streaming is complete
              if (choice.finish_reason) {
                  if (choice.finish_reason === "stop") {
                      // No tools were called, just regular response
                      console.log(`Response: ${streamingContent}`);
                      messages.push({ role: "assistant", content: streamingContent });
                      break;
                  } else if (choice.finish_reason === "tool_calls") {
                      console.log(`Tool calls collected: ${functionCalls.length}`);
                      break;
                  }
              }
          }

          // Step 3: Reconstruct and append assistant message with multiple tool calls
          const toolCallsForMessage = [];
          for (const funcCall of functionCalls) {
              toolCallsForMessage.push({
                  id: funcCall.call_id,
                  type: "function",
                  function: {
                      name: funcCall.name,
                      arguments: funcCall.arguments
                  }
              });
          }

          const assistantMessage = {
              role: "assistant",
              content: null,
              tool_calls: toolCallsForMessage
          };
          messages.push(assistantMessage);

          // Step 4: Execute all functions and append results
          for (const functionCall of functionCalls) {
              const functionName = functionCall.name;

              console.log(`Processing tool call: ${functionName}`);

              let functionResponse;
              if (functionName === "calculate_mean") {
                  try {
                      const argumentsDict = JSON.parse(functionCall.arguments);
                      functionResponse = calculateMean(argumentsDict.numbers);
                  } catch (error) {
                      functionResponse = `Error: ${error.message}`;
                  }
              } else if (functionName === "calculate_standard_deviation") {
                  try {
                      const argumentsDict = JSON.parse(functionCall.arguments);
                      functionResponse = calculateStandardDeviation(argumentsDict.numbers);
                  } catch (error) {
                      functionResponse = `Error: ${error.message}`;
                  }
              } else {
                  functionResponse = `Unknown function: ${functionName}`;
              }

              console.log(`Function response: ${functionResponse}`);

              // Append tool result to conversation history
              messages.push({
                  role: "tool",
                  content: functionResponse.toString(),
                  tool_call_id: functionCall.call_id,
                  name: functionName,
              });
          }

          // Step 5: Get the final response
          const finalResponse = await client.chat.chat({
              model: "palmyra-x5",
              messages: messages,
              stream: true
          });

          let finalContent = "";
          for await (const chunk of finalResponse) {
              const choice = chunk.choices[0];
              if (choice.delta && choice.delta.content) {
                  finalContent += choice.delta.content;
                  process.stdout.write(choice.delta.content);
              }
          }

          console.log(`\nFinal response: ${finalContent}`);

          // Step 6: Append final response to conversation history
          messages.push({
              role: "assistant",
              content: finalContent
          });
      }

      main();
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Non-streaming">
    ### Non-streaming

    The processing logic for multiple tool calls is identical to single tool calls - the same code handles both scenarios. The only difference is that the loop for checking the response for tool calling processes multiple tool calls instead of one.

    #### Step 1: Get and append the assistant's response

    First, get the assistant's response message and append it to your conversation history. This is crucial for maintaining context.

    <CodeGroup>
      ```python Python theme={null}
      response_message = response.choices[0].message
      messages.append(response_message)  # Always append the assistant's response
      tool_calls = response_message.tool_calls
      ```

      ```js JavaScript theme={null}
      const responseMessage = response.choices[0].message;
      messages.push(responseMessage);  // Always append the assistant's response
      const toolCalls = responseMessage.tool_calls;
      ```
    </CodeGroup>

    #### Step 2: Execute functions and append tool results

    Next, check for tool calls and execute the corresponding functions with the arguments provided by the model. Then, append the tool results to the conversation history.

    <CodeGroup>
      ```python Python theme={null}
      tool_calls = response_message.tool_calls
      if tool_calls:
          for tool_call in tool_calls:
              tool_call_id = tool_call.id
              function_name = tool_call.function.name
              function_args = json.loads(tool_call.function.arguments)

              # Execute the appropriate function based on the tool call
              if function_name == "calculate_mean":
                  function_response = calculate_mean(function_args["numbers"])
              elif function_name == "calculate_standard_deviation":
                  function_response = calculate_standard_deviation(function_args["numbers"])
              else:
                  function_response = f"Unknown function: {function_name}"

              # Append tool result to conversation history
              messages.append({
                  "role": "tool",
                  "tool_call_id": tool_call_id,
                  "name": function_name,
                  "content": str(function_response),
              })
      else:
          # No tool calls, just use the original response
          print(f"Response: {response_message.content}")
      ```

      ```js JavaScript theme={null}
      const toolCalls = responseMessage.tool_calls;
      if (toolCalls && toolCalls.length > 0) {
          for (const toolCall of toolCalls) {
              const toolCallId = toolCall.id;
              const functionName = toolCall.function.name;
              const functionArgs = JSON.parse(toolCall.function.arguments);

              // Execute the appropriate function based on the tool call
              let functionResponse;
              if (functionName === "calculate_mean") {
                  functionResponse = calculateMean(functionArgs.numbers);
              } else if (functionName === "calculate_standard_deviation") {
                  functionResponse = calculateStandardDeviation(functionArgs.numbers);
              } else {
                  functionResponse = `Unknown function: ${functionName}`;
              }

              // Append tool result to conversation history
              messages.push({
                  role: "tool",
                  tool_call_id: toolCallId,
                  name: functionName,
                  content: functionResponse.toString(),
              });
          }
      } else {
          // No tool calls, just use the original response
          console.log(`Response: ${responseMessage.content}`);
      }
      ```
    </CodeGroup>

    #### Step 3: Get and append the final response

    After appending tool results, get the final response from the model and append it to maintain complete conversation history:

    <CodeGroup>
      ```python Python theme={null}
          final_response = client.chat.chat(
              model="palmyra-x5",
              messages=messages,
              stream=False
          )

          final_content = final_response.choices[0].message.content
          print(f"Final response: {final_content}")
      ```

      ```js JavaScript theme={null}
          const finalResponse = await client.chat.chat({
              model: "palmyra-x5",
              messages: messages,
              stream: false
          });

          const finalContent = finalResponse.choices[0].message.content;
          console.log(`Final response: ${finalContent}`);
      ```
    </CodeGroup>

    #### Step 4: Append final response to conversation history

    Append the final response to maintain complete conversation history:

    <CodeGroup>
      ```python Python theme={null}
          # Append final response to conversation history
          messages.append({
              "role": "assistant",
              "content": final_content
          })
      ```

      ```js JavaScript theme={null}
          // Append final response to conversation history
          messages.push({
              role: "assistant",
              content: finalContent
          });
      ```
    </CodeGroup>

    #### Complete non-streaming code example

    <CodeGroup>
      ```python Python [expandable] theme={null}
      import json
      import math
      from writerai import Writer

      # Initialize the Writer client. If you don't pass the `apiKey` parameter,
      # the client looks for the `WRITER_API_KEY` environment variable.
      client = Writer()

      def calculate_mean(numbers: list) -> float:
          if not numbers:
              raise ValueError("Cannot calculate mean of an empty list")
          return sum(numbers) / len(numbers)

      def calculate_standard_deviation(numbers: list) -> float:
          if len(numbers) < 2:
              raise ValueError("Cannot calculate standard deviation with fewer than 2 numbers")
          mean = calculate_mean(numbers)
          variance = sum((x - mean) ** 2 for x in numbers) / len(numbers)
          return math.sqrt(variance)

      tools = [
          {
              "type": "function",
              "function": {
                  "name": "calculate_mean",
                  "description": "Calculate the mean (average) of a list of numbers. Use this when asked for the average or mean of numbers.",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "numbers": {
                              "type": "array",
                              "items": {"type": "number"},
                              "description": "List of numbers to calculate the mean for"
                          }
                      },
                      "required": ["numbers"]
                  }
              }
          },
          {
              "type": "function",
              "function": {
                  "name": "calculate_standard_deviation",
                  "description": "Calculate the standard deviation of a list of numbers. Use this when asked for standard deviation, variance, or statistical spread of numbers.",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "numbers": {
                              "type": "array",
                              "items": {"type": "number"},
                              "description": "List of numbers to calculate the standard deviation for"
                          }
                      },
                      "required": ["numbers"]
                  }
              }
          }
      ]

      messages = [{"role": "user", "content": "What are the mean and standard deviation of [10, 20, 30, 40, 50]?"}]

      # Step 1: Initial request with tools
      response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          tools=tools,
          tool_choice="auto",
          stream=False
      )

      # Step 2: Get and append assistant response
      response_message = response.choices[0].message
      messages.append(response_message)
      tool_calls = response_message.tool_calls

      # Step 3: Process tool calls if any
      if tool_calls:
          for tool_call in tool_calls:
              tool_call_id = tool_call.id
              function_name = tool_call.function.name
              function_args = json.loads(tool_call.function.arguments)

              # Execute the appropriate function based on the tool call
              if function_name == "calculate_mean":
                  function_response = calculate_mean(function_args["numbers"])
              elif function_name == "calculate_standard_deviation":
                  function_response = calculate_standard_deviation(function_args["numbers"])
              else:
                  function_response = f"Unknown function: {function_name}"

              # Append tool result to conversation history
              messages.append({
                  "role": "tool",
                  "tool_call_id": tool_call_id,
                  "name": function_name,
                  "content": str(function_response),
              })

          # Step 4: Get final response
          final_response = client.chat.chat(
              model="palmyra-x5",
              messages=messages,
              stream=False
          )

          final_content = final_response.choices[0].message.content
          print(f"Final response: {final_content}")

          # Step 5: Append final response to conversation history
          messages.append({
              "role": "assistant",
              "content": final_content
          })
      else:
          # No tool calls, just use the original response
          print(f"Response: {response_message.content}")
      ```

      ```js JavaScript [expandable] theme={null}
      import { Writer } from "writer-sdk";

      // Initialize the Writer client. If you don't pass the `apiKey` parameter,
      // the client looks for the `WRITER_API_KEY` environment variable.
      const client = new Writer();

      function calculateMean(numbers) {
          if (numbers.length === 0) {
              throw new Error("Cannot calculate mean of an empty array");
          }
          return numbers.reduce((sum, num) => sum + num, 0) / numbers.length;
      }

      function calculateStandardDeviation(numbers) {
          if (numbers.length < 2) {
              throw new Error("Cannot calculate standard deviation with fewer than 2 numbers");
          }
          const mean = calculateMean(numbers);
          const variance = numbers.reduce((sum, num) => sum + Math.pow(num - mean, 2), 0) / numbers.length;
          return Math.sqrt(variance);
      }

      const tools = [
          {
              type: "function",
              function: {
                  name: "calculate_mean",
                  description: "Calculate the mean (average) of a list of numbers. Use this when asked for the average or mean of numbers.",
                  parameters: {
                      type: "object",
                      properties: {
                          numbers: {
                              type: "array",
                              items: { type: "number" },
                              description: "List of numbers to calculate the mean for"
                          }
                      },
                      required: ["numbers"]
                  }
              }
          },
          {
              type: "function",
              function: {
                  name: "calculate_standard_deviation",
                  description: "Calculate the standard deviation of a list of numbers. Use this when asked for standard deviation, variance, or statistical spread of numbers.",
                  parameters: {
                      type: "object",
                      properties: {
                          numbers: {
                              type: "array",
                              items: { type: "number" },
                              description: "List of numbers to calculate the standard deviation for"
                          }
                      },
                      required: ["numbers"]
                  }
              }
          }
      ];

      async function main() {
          let messages = [{ role: "user", content: "What are the mean and standard deviation of [10, 20, 30, 40, 50]?" }];

          // Step 1: Initial request with tools
          const response = await client.chat.chat({
              model: "palmyra-x5",
              messages: messages,
              tools: tools,
              tool_choice: "auto",
              stream: false
          });

          // Step 2: Get and append assistant response
          const responseMessage = response.choices[0].message;
          messages.push(responseMessage);
          const toolCalls = responseMessage.tool_calls;

          // Step 3: Process tool calls if any
          if (toolCalls && toolCalls.length > 0) {
              for (const toolCall of toolCalls) {
                  const toolCallId = toolCall.id;
                  const functionName = toolCall.function.name;
                  const functionArgs = JSON.parse(toolCall.function.arguments);

                  // Execute the appropriate function based on the tool call
                  let functionResponse;
                  if (functionName === "calculate_mean") {
                      functionResponse = calculateMean(functionArgs.numbers);
                  } else if (functionName === "calculate_standard_deviation") {
                      functionResponse = calculateStandardDeviation(functionArgs.numbers);
                  } else {
                      functionResponse = `Unknown function: ${functionName}`;
                  }

                  // Append tool result to conversation history
                  messages.push({
                      role: "tool",
                      tool_call_id: toolCallId,
                      name: functionName,
                      content: functionResponse.toString(),
                  });
              }

              // Step 4: Get final response
              const finalResponse = await client.chat.chat({
                  model: "palmyra-x5",
                  messages: messages,
                  stream: false
              });

              const finalContent = finalResponse.choices[0].message.content;
              console.log(`Final response: ${finalContent}`);

              // Step 5: Append final response to conversation history
              messages.push({
                  role: "assistant",
                  content: finalContent
              });
          } else {
              // No tool calls, just use the original response
              console.log(`Response: ${responseMessage.content}`);
          }
      }

      main();
      ```
    </CodeGroup>
  </Tab>
</Tabs>

<Note>
  When processing multiple tool calls, ensure that:

  * All tool calls are executed before getting the final response
  * Each tool result is properly appended to the conversation history
  * Error handling is implemented for each tool call
  * The final response includes all tool results for context
</Note>

## Example: External API call

The following example covers a common use case for tool calling: calling an external API.

The code uses a publicly available [dictionary API](https://dictionaryapi.dev/) to return information about an English word's phonetic pronunciation.

This example is using non-streaming; for streaming, refer to the [multiple tool calls streaming example](#process-multiple-tool-calls) to adjust the code.

### Define function calling an API

First, define the function in your code. The examples below take in a word, call the dictionary API, and return the phonetic pronunciation of the word as a JSON-formatted string.

<CodeGroup>
  ```python Python theme={null}
  import requests
  import json
  def get_word_pronunciation(word):
      url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
      response = requests.get(url)
      if response.status_code == 200:
          return json.dumps(response.json()[0]['phonetics'])
      else:
          return f"Failed to retrieve word pronunciation. Status code: {response.status_code}"
  ```

  ```js JavaScript theme={null}
  async function getWordPronunciation(word) {
      const url = `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`;

      try {
          const response = await fetch(url);
          if (response.ok) {
              const data = await response.json();
              return JSON.stringify(data[0]['phonetics']);
          } else {
              return `Failed to retrieve word pronunciation. Status code: ${response.status}`;
          }
      } catch (error) {
          return `Error fetching word pronunciation: ${error.message}`;
      }
  }
  ```
</CodeGroup>

### Define tools array

Next, define a tools array that describes the tool with a JSON schema.

<CodeGroup>
  ```python Python theme={null}
  tools = [
      {
          "type": "function",
          "function": {
              "name": "get_word_pronunciation",
              "description": "A function that will return JSON containing the phonetic pronunciation of an English word",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "word": {
                          "type": "string",
                          "description": "The word to get the phonetic pronunciation for",
                      }
                  },
                  "required": ["word"],
              },
          },
      }
  ]
  ```

  ```js JavaScript theme={null}
  const tools = [
      {
          type: "function",
          function: {
              name: "get_word_pronunciation",
              description: "A function that will return JSON containing the phonetic pronunciation of an English word",
              parameters: {
                  type: "object",
                  properties: {
                      word: {
                          type: "string",
                          description: "The word to get the phonetic pronunciation for"
                      }
                  },
                  required: ["word"]
              }
          }
      }
  ];
  ```
</CodeGroup>

### Pass the tools to the model

Call the `chat.chat` method with the `tools` parameter set to the `tools` array and `tool_choice` set to `auto`.

<CodeGroup>
  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  messages = [{"role": "user", "content": "what is the phonetic pronunciation of the word 'epitome' in English?"}]

  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto",
      stream=False
  )
  ```

  ```js JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  let messages = [{role: "user", content: "what is the phonetic pronunciation of the word 'epitome' in English?"}];

  const response = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      tools: tools,
      tool_choice: "auto",
      stream: false
  });
  ```
</CodeGroup>

### Check response for tool calling

Loop through the `tool_calls` array to check for the invocation of the tool. Then, call the tool's function with the arguments the model provided.

<CodeGroup>
  ```python Python theme={null}
  response_message = response.choices[0].message
  messages.append(response_message)
  tool_calls = response_message.tool_calls
  if tool_calls:
      tool_call = tool_calls[0]
      tool_call_id = tool_call.id
      function_name = tool_call.function.name
      function_args = json.loads(tool_call.function.arguments)

      if function_name == "get_word_pronunciation":
          function_response = get_word_pronunciation(function_args["word"])
  ```

  ```js JavaScript theme={null}
  const responseMessage = response.choices[0].message;
  messages.push(responseMessage);  // Append assistant response
  const toolCalls = responseMessage.tool_calls;

  if (toolCalls && toolCalls.length > 0) {
      const toolCall = toolCalls[0];
      const toolCallId = toolCall.id;
      const functionName = toolCall.function.name;
      const functionArgs = JSON.parse(toolCall.function.arguments);

      if (functionName === "get_word_pronunciation") {
          const functionResponse = await getWordPronunciation(functionArgs.word);
      }
  }
  ```
</CodeGroup>

### Append the tool result to the conversation history

Finally, pass the result back to the model by appending it to the messages array, and get the final response.

<CodeGroup>
  ```python Python theme={null}
  messages.append({
      "role": "tool",
      "tool_call_id": tool_call_id,
      "name": function_name,
      "content": function_response,
  })

  final_response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      stream=False
  )

  final_content = final_response.choices[0].message.content

  # Append final response to conversation history
  messages.append({
      "role": "assistant",
      "content": final_content
  })

  print(f"Final response: {final_content}")
  # Final response: The phonetic pronunciation of the word "epitome" in English is /əˈpɪt.ə.mi/...
  ```

  ```js JavaScript theme={null}
  messages.push({
      role: "tool",
      tool_call_id: toolCallId,
      name: functionName,
      content: functionResponse,
  });

  const finalResponse = await client.chat.chat({
      model: "palmyra-x5",
      messages: messages,
      stream: false
  });

  const finalContent = finalResponse.choices[0].message.content;

  // Append final response to conversation history
  messages.push({
      role: "assistant",
      content: finalContent
  });

  console.log(`Final response: ${finalContent}`);
  // Final response: The phonetic pronunciation of the word "epitome" in English is /əˈpɪt.ə.mi/...
  ```
</CodeGroup>

### Complete external API call example

<CodeGroup>
  ```python Python [expandable] theme={null}
  import requests
  import json
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `apiKey` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  def get_word_pronunciation(word):
      url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
      try:
          response = requests.get(url)
          if response.status_code == 200:
              return json.dumps(response.json()[0]['phonetics'])
          else:
              return f"Failed to retrieve word pronunciation. Status code: {response.status_code}"
      except Exception as e:
          return f"Error fetching word pronunciation: {str(e)}"

  tools = [
      {
          "type": "function",
          "function": {
              "name": "get_word_pronunciation",
              "description": "A function that will return JSON containing the phonetic pronunciation of an English word",
              "parameters": {
                  "type": "object",
                  "properties": {
                      "word": {
                          "type": "string",
                          "description": "The word to get the phonetic pronunciation for",
                      }
                  },
                  "required": ["word"],
              },
          },
      }
  ]

  messages = [{"role": "user", "content": "what is the phonetic pronunciation of the word 'epitome' in English?"}]

  # Step 1: Initial request
  response = client.chat.chat(
      model="palmyra-x5",
      messages=messages,
      tools=tools,
      tool_choice="auto",
      stream=False
  )

  # Step 2: Append assistant response
  response_message = response.choices[0].message
  messages.append(response_message)
  tool_calls = response_message.tool_calls

  # Step 3: Process tool calls
  if tool_calls:
      for tool_call in tool_calls:
          tool_call_id = tool_call.id
          function_name = tool_call.function.name
          function_args = json.loads(tool_call.function.arguments)

          if function_name == "get_word_pronunciation":
              function_response = get_word_pronunciation(function_args["word"])

              # Append tool result
              messages.append({
                  "role": "tool",
                  "tool_call_id": tool_call_id,
                  "name": function_name,
                  "content": function_response,
              })

      # Step 4: Get final response
      final_response = client.chat.chat(
          model="palmyra-x5",
          messages=messages,
          stream=False
      )

      final_content = final_response.choices[0].message.content

      # Step 5: Append final response
      messages.append({
          "role": "assistant",
          "content": final_content
      })

      print(f"Final response: {final_content}")

  # The conversation history is now complete and ready for additional turns
  ```

  ```javascript JavaScript [expandable] theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  async function getWordPronunciation(word) {
      const url = `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`;

      try {
          const response = await fetch(url);
          if (response.ok) {
              const data = await response.json();
              return JSON.stringify(data[0]['phonetics']);
          } else {
              return `Failed to retrieve word pronunciation. Status code: ${response.status}`;
          }
      } catch (error) {
          return `Error fetching word pronunciation: ${error.message}`;
      }
  }

  const tools = [
      {
          type: "function",
          function: {
              name: "get_word_pronunciation",
              description: "A function that will return JSON containing the phonetic pronunciation of an English word",
              parameters: {
                  type: "object",
                  properties: {
                      word: {
                          type: "string",
                          description: "The word to get the phonetic pronunciation for"
                      }
                  },
                  required: ["word"]
              }
          }
      }
  ];

  async function main() {
      let messages = [{
          role: "user",
          content: "what is the phonetic pronunciation of the word 'epitome' in English?"
      }];

      // Step 1: Initial request
      const response = await client.chat.chat({
          model: "palmyra-x5",
          messages: messages,
          tools: tools,
          tool_choice: "auto",
          stream: false
      });

      // Step 2: Append assistant response
      const responseMessage = response.choices[0].message;
      messages.push(responseMessage);
      const toolCalls = responseMessage.tool_calls;

      // Step 3: Process tool calls
      if (toolCalls && toolCalls.length > 0) {
          for (const toolCall of toolCalls) {
              const toolCallId = toolCall.id;
              const functionName = toolCall.function.name;
              const functionArgs = JSON.parse(toolCall.function.arguments);

              if (functionName === "get_word_pronunciation") {
                  const functionResponse = await getWordPronunciation(functionArgs.word);

                  // Append tool result
                  messages.push({
                      role: "tool",
                      tool_call_id: toolCallId,
                      name: functionName,
                      content: functionResponse,
                  });
              }
          }

          // Step 4: Get final response
          const finalResponse = await client.chat.chat({
              model: "palmyra-x5",
              messages: messages,
              stream: false
          });

          const finalContent = finalResponse.choices[0].message.content;

          // Step 5: Append final response
          messages.push({
              role: "assistant",
              content: finalContent
          });

          console.log(`Final response: ${finalContent}`);
      }
  }

  main();
  ```
</CodeGroup>

## Next steps

By following this guide, you can incorporate tool calling into your application and augment the capabilities of a model with real-time data, math operations, business logic, and much more. For more examples, check out the [tool calling cookbooks](https://github.com/writer/cookbooks/tree/main/tool_calling) available on GitHub.

Next, learn how to invoke [no-code agents with tool calling](/home/applications-tool-calling). Or, explore prebuilt tools that Writer models can execute remotely:

* [Ask questions to a Knowledge Graph in a chat](/home/kg-chat)
* [Delegate questions to another model](/home/model-delegation)
* [Search the web](/home/web-search-tool)
* [Translate text](/home/translation-tool)


# Toxic check
Source: https://dev.writer.com/home/toxic-check



We developed an independent system that classifies input and output textual data and predicts its level of toxicity. Our model was trained on a distinct dataset that included both toxic and nontoxic examples. Each prompt is sent through the API to one of our large language models, and the generated text is analyzed and classified by a model that predicts the text's toxicity level. This probability lies between 0 and 1, where 0 indicates toxic classes and 1 indicates nontoxic classes

The Palmyra models provide scores for several different attributes, including Toxicity. Here are some of the other attributes Palmyra can provide scores for:

* **Severe Toxicity**: A very hateful, aggressive, disrespectful comment or otherwise very likely to make a user leave a discussion or give up on sharing their perspective. This attribute is much less sensitive to more mild forms of toxicity, such as comments that include positive uses of curse words.
* **Insult**: Insulting, inflammatory, or negative comment towards a person or a group of people.
* **Profanity**: Swear words, curse words, or other obscene or profane language.
* **Identity Attack**: Negative or hateful comments targeting someone because of their identity.
* **Threat**: Describes an intention to inflict pain, injury, or violence against an individual or group.
* **Sexually explicit**: Contains references to sexual acts, body parts, or other lewd content.

The text completion API methods return a JSON response with two fields, prompt labels and completion labels, that indicate the toxicity scores for each part independently. The field class name contains a binary class label determined by score, where a value less than 0.5 indicates toxicity and a value greater than 0.5 indicates nontoxicity.

```json theme={null}

"prompt_labels": [
   {
      "class_name": "nontoxic",
      "score": 0.995605
    }
  ],
"completion_labels": [
    {
      "class_name": "nontoxic",
      "score": 0.997559    }
  ]
```


# Translate text in a chat
Source: https://dev.writer.com/home/translation-tool



The translation tool for chat completions allows you to translate text during a conversation.

While Palmyra X models can perform translation tasks, they are not optimized for these tasks and may not perform well without correct prompting. Palmyra Translate is a dedicated model optimized for translation use cases.

This guide explains how to use the translation tool in a chat completion and provides an example of how to use it.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Tool structure

The translation tool allows you to translate text during a [chat completion](/home/chat-completion).

To use the translation tool, add it to the `tools` array in your `chat-completion` endpoint request.

The translation tool object has the following structure:

| Parameter                  | Type      | Description                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                     | `string`  | The type of tool, which is `translation` for the translation tool                                                                                                                                                                                                                                                                                                                                                     |
| `function`                 | `object`  | An object containing the tool's description and model                                                                                                                                                                                                                                                                                                                                                                 |
| `function.model`           | `string`  | `palmyra-translate`                                                                                                                                                                                                                                                                                                                                                                                                   |
| `function.formality`       | `boolean` | Whether the translation should be formal or informal, [if the target language supports it](/api-reference/translation-api/language-support#formality).                                                                                                                                                                                                                                                                |
| `function.length_control`  | `boolean` | Whether to control the length of the translation, [if the target language supports it](/api-reference/translation-api/language-support#length-control).                                                                                                                                                                                                                                                               |
| `function.mask_profanity`  | `boolean` | Whether to mask profanity in the translation, [if the target language supports it](/api-reference/translation-api/language-support#profanity-masking).                                                                                                                                                                                                                                                                |
| `function.source_language` | `string`  | (Optional) The [language code](/api-reference/translation-api/language-support#language-codes) of the text you want to translate. If you don't provide a source language, the model automatically detects the language of the text you want to translate. If your message contains a different language than this value, this value overrides the language detection.                                                 |
| `function.target_language` | `string`  | (Optional) The [language code](/api-reference/translation-api/language-support#language-codes) you want to translate the text to. If you don't provide a target language, the model automatically selects the most appropriate language based on the message you provide to the chat completion endpoint. If your message contains a different language than this value, this value overrides the language selection. |

<CodeGroup>
  ```bash cURL theme={null}
  "tools": [
      {
          "type": "translation",
          "function": {
              "model": "palmyra-translate",
              "formality": false,
              "length_control": false,
              "mask_profanity": true
          }
      }  
  ]
  ```

  ```python Python theme={null}
  tools = [{
      "type": "translation",
      "function": {
          "model": "palmyra-translate",
          "formality": False,
          "length_control": False,
          "mask_profanity": True
      }
  }]
  ```

  ```js JavaScript theme={null}
  const tools = [{
      type: "translation",
      function: {
          model: "palmyra-translate",
          formality: false,
          length_control: false,
          mask_profanity: true
      }
  }]
  ```
</CodeGroup>

<Note>
  You can only pass one prebuilt tool in the `tools` array at a time. However, you can pass multiple [custom tools](/home/tool-calling) in the same request.

  Prebuilt tools are:

  * Translation tool
  * [Knowledge Graph tool](/home/kg-chat)
  * [LLM tool](/home/model-delegation)
  * [Web search tool](/home/web-search-tool)
</Note>

### Response format

For non-streaming responses, the translated text is in the `choices[0].message.content` field. For streaming responses, the translated text is in the `choices[0].delta.content` field.

The response also contains a `translation_data` field that contains the following information:

| Parameter              | Type     | Description                                                         |
| ---------------------- | -------- | ------------------------------------------------------------------- |
| `source_language_code` | `string` | The language code of the text you provided to the translation tool. |
| `target_language_code` | `string` | The language code of the translated text.                           |
| `source_text`          | `string` | The text the translation tool translated.                           |

<Warning>
  You can use the translation tool with streaming chat responses, but the translation tool call is not streamed. The response comes back in the `choices[0].delta.content` field once the full translation is complete.

  If you are unfamiliar with the chat completions endpoint or streaming vs. non-streaming responses, learn more in the [chat completion guide](/home/chat-completion).
</Warning>

See the [chat completion endpoint](/api-reference/completion-api/chat-completion#response-id) for more information on the response fields.

<CodeGroup>
  ```json non-streaming response {9,19-23} theme={null}
  {
    "id": "63ae5c5d",
    "object": "chat.completion",
    "choices": [
      {
        "index": 0,
        "finish_reason": "tool_calls",
        "message": {
          "content": "¡Hola, mundo!",
          "role": "assistant",
          "tool_calls": null,
          "graph_data": {
            "sources": null,
            "status": null,
            "subqueries": null
          },
          "llm_data": null,
          "image_data": null,
          "translation_data": {
            "source_text": "Hello, world!",
            "source_language_code": "en",
            "target_language_code": "es"
          },
          "refusal": null
        },
        "logprobs": null
      }
    ],
    "created": 1745956341,
    "model": "palmyra-x5",
    "usage": {
      "prompt_tokens": 2909,
      "total_tokens": 2945,
      "completion_tokens": 36,
      "prompt_token_details": null,
      "completion_tokens_details": null
    },
    "system_fingerprint": "v1",
    "service_tier": null
  }
  ```

  ```json streaming response {26,36-40} theme={null}
  {
      "id": "8cbef9eb",
      "object": "chat.completion.chunk",
      "choices": [{
          "index": 0,
          "finish_reason": "tool_calls",
          "message": {
              "content": "¡Hola, mundo!",
              "role": "assistant",
              "tool_calls": null,
              "graph_data": {
                  "sources": null,
                  "status": null,
                  "subqueries": null
              },
              "llm_data": null,
              "image_data": null,
              "translation_data": {
                  "source_text": "Hello, world!",
                  "source_language_code": "en",
                  "target_language_code": "es"
              },
              "refusal": null
          },
          "delta": {
              "content": "¡Hola, mundo!",
              "role": "assistant",
              "tool_calls": null,
              "graph_data": {
                  "sources": null,
                  "status": null,
                  "subqueries": null
              },
              "llm_data": null,
              "image_data": null,
              "translation_data": {
                  "source_text": "Hello, world!",
                  "source_language_code": "en",
                  "target_language_code": "es"
              },
              "refusal": null
          },
          "logprobs": null
      }],
      "created": 1745956498,
      "model": "palmyra-x5",
      "usage": null,
      "system_fingerprint": "v1",
      "service_tier": null
  }
  ```
</CodeGroup>

## Usage example

This example uses `palmyra-translate` to translate a message during a chat completion.

### Create a tools array containing a translation tool

First, create a `tools` array that specifies the translation tool you want to use.

<CodeGroup>
  ```bash cURL theme={null}
  "tools": [
      {
          "type": "translation",
          "function": {
              "model": "palmyra-translate",
              "formality": false,
              "length_control": false,
              "mask_profanity": true
          }
      }
  ]
  ```

  ```python Python theme={null}
  tools = [{
      "type": "translation",
      "function": {
          "model": "palmyra-translate",
          "formality": False,
          "length_control": False,
          "mask_profanity": True
      }
  }]
  ```

  ```js JavaScript theme={null}
  const tools = [{
      type: "translation",
      function: {
          model: "palmyra-translate",
          formality: false,
          length_control: false,
          mask_profanity: true
          ]
      }
  }]
  ```
</CodeGroup>

### Send the request using chat completions

Add the tools array to the chat endpoint call along with your array of messages. Setting `tool_choice` to `auto` allows the model to choose when to use the translation tool, based on the message provided in the `messages` array.

The response contains the translated text in the `choices[0].message.content` field.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "model": "palmyra-x5",
          "messages": [
              {
                  "role": "user",
                  "content": "Translate the following message to Spanish: Hello, world!"
              }
          ],
          "tool_choice": "auto",
          "tools": [
              {
                  "type": "translation",
                  "function": {
                      "model": "palmyra-translate",
                      "formality": false,
                      "length_control": false,
                      "mask_profanity": true
                      }
                  }
          ]
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  messages = [{"role": "user", "content": "Translate the following message to Spanish: 'Hello, world!'"}]

  response = client.chat.chat(
      model="palmyra-x5", 
      messages=messages, 
      tools=tools,  # The tools array defined earlier.
      tool_choice="auto"
  )

  print(response.choices[0].message.content)
  ```

  ```js JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const messages = [{role: "user", content: "Translate the following message to Spanish: 'Hello, world!'"}];

  const response = await client.chat.chat({
      model: "palmyra-x5", 
      messages: messages, 
      tools: tools, // The tools array defined earlier.
      tool_choice: "auto"
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

If you want to verify the translation data in the response, you can print the `translation_data` field to see the source text, source language code, and target language code that the translation tool used.

<CodeGroup>
  ```python Python theme={null}
  print(response.choices[0].message.translation_data)
  ```

  ```js JavaScript theme={null}
  console.log(response.choices[0].message.translation_data);
  ```
</CodeGroup>

By following this guide, you can use the translation tool to have the `palmyra-translate` model translate a message during a chat completion.

## Next steps

Learn about additional capabilities of the Writer API, such as [analyzing images](/home/chat-with-images) and [web search](/home/web-search).


# Transparency
Source: https://dev.writer.com/home/transparency



Ensuring transparency and accountability in AI systems

## Introduction

The complexity of deep learning models like Palmyra-X makes them remarkably powerful but also increasingly opaque. As the adoption of these AI technologies expands, the necessity for transparency and accountability becomes critical. Writer addresses these concerns by employing a multi-tiered approach that leverages cutting-edge algorithms and technologies. This article provides a detailed technical perspective on these strategies.

## Tools and algorithms for insights into decision-making

<Tabs>
  <Tab title="Attention mechanism analysis">
    In transformer-based models like Palmyra, attention mechanisms play a crucial role in determining output. By visualizing the weights in the multi-headed attention layers, one can gain insights into which input tokens significantly influence the output. Algorithms like layer-wise relevance propagation can be employed to decompose these attention scores.
  </Tab>

  <Tab title="SHAP">
    SHAP (SHapley Additive exPlanations) values are derived from cooperative game theory and offer a unified measure of feature importance. By computing SHAP values for each feature in the input data, one can quantify how much each feature contributes to a particular decision, thus offering a granular view of the model's inner workings.
  </Tab>

  <Tab title="Comprehensive logging">
    A robust logging system captures not just input-output pairs but also intermediate representations, attention maps and activation functions. Tools like TensorBoard with custom dashboards are used for real-time monitoring and auditing at testing stage.
  </Tab>

  <Tab title="Bayesian A/B testing">
    Traditional A/B testing is enhanced with Bayesian statistical methods to rigorously compare the performance and decision-making processes of different model versions. This provides confidence intervals and posterior distributions that offer more nuanced insights than point estimates.
  </Tab>
</Tabs>

## Addressing the 'opaque' nature of AI: explainability and transparency

<Tabs>
  <Tab title="LIME">
    Local Interpretable Model-agnostic Explanations (LIME) is used to create surrogate models that approximate the behavior of the complex model in the vicinity of the instance being explained. By perturbing the input and observing the output, LIME fits a simple model that is easier to interpret, thus shedding light on the original model's decision-making process.
  </Tab>

  <Tab title="Counterfactual Explanations">
    Counterfactual explanations provide "what-if" scenarios that help understand how a different input could lead to a different output. We use DiCE Algorithm (Diverse Counterfactual Explanations) to generate these scenarios.
  </Tab>

  <Tab title="Open Source and Community Auditing">
    Releasing the models under an open-source Apache 2.0 license allows for community-based auditing. Skilled developers and researchers can scrutinize the codebase, algorithms, and even contribute to enhancing transparency features.
  </Tab>

  <Tab title="Differential Privacy">
    To safeguard user data while maintaining transparency, differential privacy algorithms like Laplace noise addition or Differential Privacy SGD are implemented. This allows the model to be queried for insights without revealing any individual data points.
  </Tab>
</Tabs>

## Compliance and regulations

For regulatory compliance, techniques such as Automatic Fairness Verification and Fairness-aware Learning are integrated into the model training pipeline. These ensure that the model meets standards like GDPR, which mandates the right to explanation for automated decisions.

## Conclusion

Transparency and accountability in AI models are complex challenges that require a multi-layered, algorithmically robust approach. By employing advanced techniques like SHAP, LIME‌ and Bayesian A/B testing, Writer aims to open up the "black box" of its AI models. While full transparency remains a moving target, these technologies and methodologies provide a comprehensive framework for making significant strides in understanding and auditing AI systems.


# Usage and spend
Source: https://dev.writer.com/home/usage-and-spend

Track organization-wide costs, token usage, and consumption trends across all agents

The Consumption view in AI Studio gives billing administrators and finance teams a detailed breakdown of usage and spend month-by-month across all agents. Track costs by agent type, model, and service to understand where your organization's AI budget is going.

<Info>
  The Consumption view is only available to [organization
  admins](https://support.writer.com).
</Info>

## View your consumption data

To access the Consumption view, [log in to AI Studio](https://app.writer.com/aistudio) and select the **Consumption** tab in the left sidebar.

<img alt="Consumption view in AI Studio" />

You can switch between a cost view and an activity view, which shows the number of tokens used for each agent type. See below for more details about the data available in the [cost view](#cost-view) and the [activity view](#activity-view).

## Cost view

The cost view shows the following information:

* **Monthly usage-based spend**: The total cost of usage for the month, with a comparison to the previous month.
* **Spend by agent type**: A day-by-day breakdown of the cost of usage by agent type (API, Framework, and No-Code).
* **Spend breakdown**:
  * **LLM models**: The usage cost for each LLM model.
  * **Others**: The usage cost for all other services, such as Knowledge Graph hosting, web access, and optical character recognition (OCR).

## Activity view

The activity view shows the following information:

* **Monthly token usage**: The total number of tokens used for the month, with a comparison to the previous month.
* **Top users by token usage**: The top five users with the most token usage.
* **Top agents by token usage**: The top five agents with the most token usage.
* **Token usage by agent type**: A day-by-day breakdown of the token usage by agent type (API, Framework, and No-Code).
* **Token usage by model**: The total number of tokens used for each model.

## Next steps

* [Observability overview](/home/observability): Monitor events, errors, and performance across your deployment
* [Agent observability](/home/agent-observability): Track individual agent performance with session logs and metrics


# Usage policy
Source: https://dev.writer.com/home/usage-policy



Please review the following policies to ensure responsible usage of the Writer API:

## Content Policy

The Writer API doesn't allow for the generation of the following types of content:

* Hate
* Harassment
* Violence
* Self-harm
* Sexual
* Political
* Spam
* Deception
* Malware

## Platform Policy

* The API is provided for the purpose of accessing the services, and users are expected to use it in accordance with this policy.
* Users are responsible for ensuring the security and confidentiality of their API key and any data obtained through the API.
* The API may be subject to rate limiting to ensure fair use and prevent excessive use of the system.
* The API may be temporarily unavailable for maintenance or upgrades without prior notice.
* The API is provided for commercial use, and users are expected to comply with all applicable laws and regulations.
* Users may not use the API to engage in any illegal, fraudulent, or malicious activities.
* Users may not sell, resell, or lease the API or the data obtained through the API to third parties.
* Failure to comply with this policy may result in suspension or termination of access to the API.

Writer reserves the right to take necessary measures to enforce this policy and protect the system and other users from harm.

This policy may be revised at any time without prior notice. Users are responsible for regularly checking the policy for updates.


# Add web content to a Knowledge Graph
Source: https://dev.writer.com/home/web-connector-url



This guide demonstrates how to add web content to a [Knowledge Graph](/home/knowledge-graph) using web connector URLs. Adding web connector URLs to your Knowledge Graph allows you to automatically extract and index content from websites, making it available for querying in your Knowledge Graph.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

<Note>
  Knowledge Graphs [deployed to a specific team](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph#Managing-a-Knowledge-Graph-R4Mg5) aren't accessible via the API or SDK. To use a Knowledge Graph via the API or SDK, configure it with "All Teams" access in [AI Studio](https://app.writer.com/aistudio).
</Note>

## Overview

Adding URLs to your Knowledge Graph enables you to:

* Automatically extract content from websites
* Index web pages for Knowledge Graph queries
* Process single pages or entire sub-pages
* Monitor the status of URL processing
* Exclude specific URLs from processing

## Add URLs to a Knowledge Graph

To add web connector URLs to your Knowledge Graph, use the [PUT endpoint](/api-reference/kg-api/update-graph) to update the graph with the URLs you want to process. This endpoint allows you to add, update, or remove URLs from your Knowledge Graph.

<Warning>
  You can only add URLs to a Knowledge Graph with the `type` set to `web`. To create a Knowledge Graph with the `web` type, you must create the Knowledge Graph in [AI Studio](https://app.writer.com/ai-studio) and select "Add Website" as the way you want to add content to the Knowledge Graph. See [Setting up Knowledge Graph web connectors](https://support.writer.com/article/272-setting-up-knowledge-graph-web-connectors) for more information.

  You can see the type of a Knowledge Graph in the [Knowledge Graph details](/api-reference/kg-api/retrieve-graph) or in the [Knowledge Graph list](/api-reference/kg-api/list-graphs) endpoints.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  # Note that this operation replaces the entire list of URLs attached to the Knowledge Graph.
  curl --location --request PUT 'https://api.writer.com/v1/graphs/{graph_id}' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "urls": [
              {
                  "url": "https://example.com/documentation",
                  "type": "sub_pages",
                  "exclude_urls": [
                      "https://example.com/documentation/admin",
                      "https://example.com/documentation/private"
                  ]
              }
          ]
      }'
  ```

  ```python python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  # Note that this operation replaces the entire list of URLs attached to the Knowledge Graph with the new list of URLs.
  client.graphs.update(
      "GRAPH_ID",
      urls=[{
          "url": "https://example.com/documentation",
          "type": "sub_pages",
          "exclude_urls": [
              "https://example.com/documentation/admin",
              "https://example.com/documentation/private"
          ]
      }]
  )

  print(client.graphs.retrieve("GRAPH_ID").urls)
  ```

  ```javascript javascript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  // Note that this operation replaces the entire list of URLs attached to the Knowledge Graph with the new list of URLs.
  const response = await client.graphs.update('GRAPH_ID', {
    urls: [{
      url: 'https://example.com/documentation',
      type: 'sub_pages',
      exclude_urls: [
        'https://example.com/documentation/admin',
        'https://example.com/documentation/private'
      ]
    }]
  });

  console.log(response.urls);
  ```
</CodeGroup>

### Request structure

<Warning>
  The `urls` array in your request **replaces** the entire existing list of URLs attached to the Knowledge Graph. To add a new URL, include both the existing URLs and the new one in your request. To remove a URL, send a request with all URLs except the one you want to remove.
</Warning>

When updating a Knowledge Graph with URLs, you need to provide a `urls` array in your request body. Each URL object has the following parameters:

| Parameter      | Type   | Description                                                                                                      |
| -------------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `url`          | string | The URL you want to add to your Knowledge Graph                                                                  |
| `type`         | string | How you want to process the URL: either `single_page` (just this URL) or `sub_pages` (this URL and linked pages) |
| `exclude_urls` | array  | Optional. Any URLs you want to exclude from processing if `type` is `sub_pages`                                  |

### Response structure

After you update the Knowledge Graph, you see the status of each URL in the `status` object. The `status` object has the following parameters:

| Parameter    | Type   | Description                                                        |
| ------------ | ------ | ------------------------------------------------------------------ |
| `status`     | string | The current processing status: `validating`, `success`, or `error` |
| `error_type` | string | If there was an error, what type of error occurred                 |

```json theme={null}
{
  "id": "51b058d8",
  "created_at": "2025-07-16T20:19:23.565884Z",
  "name": "Public docs",
  "description": "Public docs",
  "urls": [
    {
      "url": "https://example.com/docs",
      "status": {
        "status": "validating",
        "error_type": null
      },
      "exclude_urls": [
        "https://example.com/docs/admin",
        "https://example.com/docs/private"
      ],
      "type": "sub_pages"
    }
  ]
}
```

#### Possible error types

If URL processing fails, you may see these error types:

* `invalid_url`: The URL format is not valid
* `not_searchable`: The content cannot be accessed or processed
* `not_found`: The URL returns a 404 error
* `paywall_or_login_page`: The content requires authentication
* `unexpected_error`: An unspecified error occurred

## Usage examples

The following examples show how to add a web connector URL to a Knowledge Graph and monitor its processing status.

### Add a web connector URL

This example adds a web connector URL to a Knowledge Graph. It first retrieves the Knowledge Graph to see the current URLs and then adds a new URL to the Knowledge Graph. Note that this operation replaces the entire list of URLs attached to the Knowledge Graph, which is why it is important to retrieve the existing list of URLs first.

<CodeGroup>
  ```bash cURL theme={null}
  # 1. First, retrieve the current Knowledge Graph to see existing URLs
  curl --location 'https://api.writer.com/v1/graphs/{graph_id}' \
      --header "Authorization: Bearer $WRITER_API_KEY"

  # 2. Then, send a new request with all the URLs you want to add. In this example, there were no existing URLs, so it's adding a single URL.
  curl --location --request PUT 'https://api.writer.com/v1/graphs/{graph_id}' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "urls": [
              {
                  "url": "https://example.com/documentation",
                  "type": "sub_pages",
                  "exclude_urls": [
                      "https://example.com/documentation/admin",
                      "https://example.com/documentation/private"
                  ]
              }
          ]
      }'
  ```

  ```python Python theme={null}

  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  graph_id = "GRAPH_ID"

  graph = client.graphs.retrieve(graph_id)
  urls = graph.urls

  urls.append({
      "url": "https://example.com/documentation",
      "type": "sub_pages",
      "exclude_urls": ["https://example.com/documentation/admin", "https://example.com/documentation/private"]
  })

  client.graphs.update(graph_id, urls=urls)

  print(client.graphs.retrieve(graph_id).urls)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const graph_id = "GRAPH_ID";

  const graph = await client.graphs.retrieve(graph_id);

  const urls = graph.urls;

  urls.push({
    url: 'https://example.com/documentation',
    type: 'sub_pages',
    exclude_urls: [
      'https://example.com/documentation/admin',
      'https://example.com/documentation/private'
    ]
  });

  const response = await client.graphs.update(graph_id, { urls });

  const updatedGraph = await client.graphs.retrieve(graph_id);

  console.log(updatedGraph.urls);
  ```
</CodeGroup>

### Delete all URLs from a Knowledge Graph

To delete all URLs from a Knowledge Graph, you can send a request with an empty `urls` array.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location --request PUT 'https://api.writer.com/v1/graphs/{graph_id}' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "urls": []
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  graph_id = "GRAPH_ID"

  client.graphs.update(graph_id, urls=[])

  print(client.graphs.retrieve(graph_id).urls)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const graph_id = "GRAPH_ID";

  const response = await client.graphs.update(graph_id, { urls: [] });

  console.log(response.urls);
  ```
</CodeGroup>

### Monitor processing status

You can check the status of your web connector URLs by retrieving the Knowledge Graph details.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/graphs/{graph_id}' \
      --header "Authorization: Bearer $WRITER_API_KEY"
  ```

  ```python python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  graph = client.graphs.retrieve("GRAPH_ID")
  print(graph.urls)
  ```

  ```javascript javascript theme={null}
  import Writer from 'writer-sdk';

  // Initialize the Writer client. If you don't pass the `api_key` parameter, 
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  async function main() {
    const graph = await client.graphs.retrieve('GRAPH_ID');

    console.log(graph.urls);
  }

  main();
  ```
</CodeGroup>

### Query the Knowledge Graph with web content

Once the web connector URLs have been processed successfully, you can query the Knowledge Graph to access the extracted content.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/graphs/{graph_id}/query' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "query": "What are the main features described in the documentation?",
          "subqueries": true
      }'
  ```

  ```python python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.graphs.question(
    graph_ids=["GRAPH_ID"],
    question="YOUR QUESTION"
  )

  print(f"Answer: {response.answer}")
  print(f"Sources: {response.sources}")
  ```

  ```javascript javascript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `api_key` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.graphs.question({
    graph_ids: ["GRAPH_ID"],
    question: "YOUR QUESTION"
  });

  console.log(`Answer: ${response.answer}`);
  console.log(`Sources: ${response.sources}`);
  ```
</CodeGroup>

## Next steps

By following this guide, you can successfully add web content to your Knowledge Graphs using web connector URLs.

Next, learn about other Knowledge Graph capabilities:

* [Query a Knowledge Graph](/home/kg-query)
* [Use a Knowledge Graph in chat](/home/kg-chat)


# Search the web
Source: https://dev.writer.com/home/web-search



<Warning>
  **Deprecation notice**: The [web search API endpoint](/api-reference/tool-api/web-search) at `/v1/tools/web-search` is deprecated and will be removed on **December 22, 2025**.

  **Migration path**: Use the [web search tool](/home/web-search-tool) in chat completions to perform web searches during conversations. The web search tool provides the same search capabilities within a chat completion workflow. See the [migration guide](/api-reference/migration-guides/web-search) for detailed instructions.
</Warning>

The Web Search API allows you to search the web for information and get real-time results. This tool is useful for finding current information, news, and facts that may not be available in your model's training data or in your Knowledge Graphs.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

## Overview

The web search tool enables you to:

* Search for current information and news
* Find factual information from reliable sources
* Get real-time data that may not be in your model's training data
* Filter results by domain, time range, and geographic location
* Control search depth and result comprehensiveness

## Request example

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/tools/web-search' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "query": "How do I get an API key for the Writer API?",
          "include_domains": ["dev.writer.com"]
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.tools.web_search(
      query="How do I get an API key for the Writer API?",
      include_domains=["dev.writer.com"]
  )

  print(response.answer)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.tools.webSearch({
      query: "How do I get an API key for the Writer API?",
      include_domains: ["dev.writer.com"]
  });

  console.log(response.answer);
  ```
</CodeGroup>

### Request parameters

| Parameter             | Type              | Required | Description                                                                                                                                               |
| --------------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`               | string            | Yes      | The search query to execute                                                                                                                               |
| `topic`               | string            | No       | The search topic category. Use `news` for current events and news articles, or `general` for broader web search. Default: `general`                       |
| `search_depth`        | string            | No       | Controls search comprehensiveness. Use `basic` for fewer but highly relevant results, or `advanced` for deeper search with more results. Default: `basic` |
| `chunks_per_source`   | integer           | No       | Only applies when `search_depth` is `advanced`. Specifies how many text segments to extract from each source. Limited to 3 chunks maximum                 |
| `max_results`         | integer           | No       | Limits the number of search results returned. Cannot exceed 20 sources                                                                                    |
| `time_range`          | string            | No       | Filters results to content published within the specified time range. Options: `day`, `week`, `month`, `year`, `d`, `w`, `m`, `y`                         |
| `days`                | integer           | No       | For news topic searches, specifies how many days of news coverage to include                                                                              |
| `include_raw_content` | string \| Boolean | No       | Controls how raw content is included. Options: `text`, `markdown`, `true` (same as markdown), `false` (not included). Default: `false`                    |
| `include_answer`      | Boolean           | No       | Whether to include a generated answer to the query in the response. Default: `true`                                                                       |
| `include_domains`     | array             | No       | Domains to include in the search                                                                                                                          |
| `exclude_domains`     | array             | No       | Domains to exclude from the search                                                                                                                        |
| `country`             | string            | No       | Localizes search results to a specific country. Only applies to general topic searches                                                                    |
| `stream`              | Boolean           | No       | Enables streaming of search results as they become available. Default: `false`                                                                            |

### Response format

The response contains the search query, generated answer if requested, and an array of sources with their URLs and raw content if requested.

| Parameter               | Type   | Description                                                                         |
| ----------------------- | ------ | ----------------------------------------------------------------------------------- |
| `query`                 | string | The search query that was submitted                                                 |
| `answer`                | string | Generated answer based on the search results. `null` if `include_answer` is `false` |
| `sources`               | array  | The search results found                                                            |
| `sources[].url`         | string | URL of the search result                                                            |
| `sources[].raw_content` | string | Raw content from the source URL. `null` if `include_raw_content` is `false`         |

```json theme={null}
{
  "query": "What are the most significant developments in artificial intelligence this year?",
  "answer": "This year has seen several significant developments...",
  "sources": [
    {
      "url": "https://www.ibm.com/think/insights/artificial-intelligence-trends",
      "raw_content": null
    },
    {
      "url": "https://www.crescendo.ai/news/latest-ai-news-and-updates",
      "raw_content": null
    },
    {
      "url": "https://www.sciencedaily.com/news/computers_math/artificial_intelligence/",
      "raw_content": null
    },
    {
      "url": "https://news.microsoft.com/source/features/ai/6-ai-trends-youll-see-more-of-in-2025/",
      "raw_content": null
    },
    {
      "url": "https://hai.stanford.edu/ai-index/2025-ai-index-report",
      "raw_content": null
    }
  ]
}
```

#### Streaming response

When streaming is enabled, the response is delivered as [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). Each event contains a payload with the query, answer, and sources array. The response is delivered as a stream of events, and the client must listen for each event to receive the complete response. Learn more about [streaming responses](/home/streaming).

Below is an example of a streaming response:

```json theme={null}
data: {"query":"What are the most significant developments in artificial intelligence this year?","answer":"","sources":[]}

data: {"query":"What are the most significant developments in artificial intelligence this year?","answer":"Several","sources":[]}

data: {"query":"What are the most significant developments in artificial intelligence this year?","answer":" significant","sources":[]}
```

## Usage examples

### General web search

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/tools/web-search' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "query": "What are the most significant developments in artificial intelligence this year?"
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.tools.web_search(
      query="What are the most significant developments in artificial intelligence this year?"
  )

  print(response.answer)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.tools.webSearch({
      query: "What are the most significant developments in artificial intelligence this year?"
  });

  console.log(response.answer);
  ```
</CodeGroup>

### News search with time filtering

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/tools/web-search' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "query": "What are the latest developments in smartphone technology?",
          "topic": "news",
          "time_range": "week", 
          "include_raw_content": "markdown",
          "max_results": 10
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.tools.web_search(
      query="What are the latest developments in smartphone technology?",
      topic="news",
      time_range="week",
      include_raw_content="markdown",
      max_results=10
  )

  print(response.answer)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.tools.webSearch({
      query: "What are the latest developments in smartphone technology?",
      topic: "news",
      time_range: "week",
      include_raw_content: "markdown",
      max_results: 10
  });
  ```
</CodeGroup>

### Domain-specific search

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/tools/web-search' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "query": "What are some good tutorials for learning machine learning?",
          "include_domains": ["github.com", "stackoverflow.com"],
          "exclude_domains": ["quora.com"],
          "search_depth": "advanced",
          "chunks_per_source": 2
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  response = client.tools.web_search(
      query="What are some good tutorials for learning machine learning?",
      include_domains=["github.com", "stackoverflow.com"],
      exclude_domains=["quora.com"],
      search_depth="advanced",
      chunks_per_source=2
  )

  print(response.answer)
  ```

  ```javascript JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const response = await client.tools.webSearch({
      query: "What are some good tutorials for learning machine learning?",
      include_domains: ["github.com", "stackoverflow.com"],
      exclude_domains: ["quora.com"],
      search_depth: "advanced",
      chunks_per_source: 2
  });

  console.log(response.answer);
  ```
</CodeGroup>

## Best practices

* **Use specific queries**: More specific queries tend to yield better results
* **Choose appropriate topic**: Use `news` for current events and `general` for broader searches
* **Filter by domains**: Use `include_domains` and `exclude_domains` to focus on reliable sources
* **Control search depth**: Use `basic` for quick searches and `advanced` for comprehensive research
* **Set time ranges**: Use `time_range` to get current information when needed
* **Limit results**: Use `max_results` to control response size and processing time

## Next steps

Learn about other tools and capabilities available in the Writer API:

* [PDF parsing](/api-reference/tool-api/pdf-parser)
* [Web search tool for chat completions](/home/web-search-tool)
* [Tool calling](/home/tool-calling)
* [Knowledge Graph queries](/home/kg-query)


# Web search in a chat
Source: https://dev.writer.com/home/web-search-tool

Search the web for real-time information during chat completions. Enable models to access current news, facts, and up-to-date data.

The web search tool for chat completions allows you to search the web for current information during a conversation.

This guide explains how to use the web search tool in a chat completion and provides an example of how to use it.

<Note>
  You need an API key to access the Writer API. Get an API key by following the steps in the [API quickstart](/home/quickstart).

  We recommend setting the API key as an environment variable in a `.env` file with the name `WRITER_API_KEY`.
</Note>

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "model": "palmyra-x5",
          "messages": [
              {
                  "role": "user",
                  "content": "What are the latest developments in AI technology?"
              }
          ],
          "tool_choice": "auto",
          "tools": [
              {
                  "type": "web_search",
                  "function": {
                      "include_domains": ["wikipedia.org", "github.com", "techcrunch.com"],
                      "exclude_domains": ["quora.com"]
                  }
              }
          ]
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  # Create the tools array containing a web search tool
  tools = [{
      "type": "web_search",
      "function": {
          "include_domains": ["wikipedia.org", "github.com", "techcrunch.com"],
          "exclude_domains": ["quora.com"]
      }
  }]

  # Create the messages array containing a user message that prompts the model to use the web search tool
  messages = [{"role": "user", "content": "What are the latest developments in AI technology?"}]

  response = client.chat.chat(
      model="palmyra-x5", 
      messages=messages, 
      tools=tools,  # The tools array defined earlier.
      tool_choice="auto"
  )

  print(response.choices[0].message.content)
  ```

  ```js JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  // Create the tools array containing a web search tool
  tools = [{
      type: "web_search",
      function: {
          include_domains: ["wikipedia.org", "github.com", "techcrunch.com"],
          exclude_domains: ["quora.com"]
      }
  }]

  // Create the messages array containing a user message that prompts the model to use the web search tool
  const messages = [{role: "user", content: "What are the latest developments in AI technology?"}];

  const response = await client.chat.chat({
      model: "palmyra-x5", 
      messages: messages, 
      tools: tools, // The tools array defined earlier.
      tool_choice: "auto"
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

## Tool structure

The web search tool allows you to search the web during a [chat completion](/home/chat-completion).

<Tip>
  To customize search parameters like including raw source text, limiting the number of sources, or specifying time ranges, include these requirements **in your prompt** to the model. The model uses these parameters when making the web search call. For example, your message to the model might include:

  * "Include raw source text"
  * "Limit the number of sources to 5"
  * "Search for news articles from the last 30 days"

  Example:

  ```json theme={null}
  "messages": [{
    "role": "user",
    "content": "Find the latest news about AI technology. Include raw source text and limit the number of sources to 5."
  }]
  ```
</Tip>

To use the web search tool, add it to the `tools` array in your `chat-completion` endpoint request.

The web search tool object has the following structure:

| Parameter                  | Type     | Description                                                     |
| -------------------------- | -------- | --------------------------------------------------------------- |
| `type`                     | `string` | The type of tool, which is `web_search` for the web search tool |
| `function`                 | `object` | An object containing the tool's configuration                   |
| `function.include_domains` | `array`  | An array of domains to include in the search results            |
| `function.exclude_domains` | `array`  | An array of domains to exclude from the search results          |

Below shows the structure of the web search tool object.

<CodeGroup>
  ```bash cURL theme={null}
  "tools": [
      {
          "type": "web_search",
          "function": {
              "include_domains": ["wikipedia.org", "github.com"],
              "exclude_domains": ["quora.com"]
          }
      }  
  ]
  ```

  ```python Python theme={null}
  tools = [{
      "type": "web_search",
      "function": {
          "include_domains": ["wikipedia.org", "github.com"],
          "exclude_domains": ["quora.com"]
      }
  }]
  ```

  ```js JavaScript theme={null}
  const tools = [{
      type: "web_search",
      function: {
          include_domains: ["wikipedia.org", "github.com"],
          exclude_domains: ["quora.com"]
      }
  }]
  ```
</CodeGroup>

<Note>
  You can only pass one prebuilt tool in the `tools` array at a time. However, you can pass multiple [custom tools](/home/tool-calling) in the same request.

  Prebuilt tools are:

  * Web search tool
  * [Knowledge Graph tool](/home/kg-chat)
  * [LLM tool](/home/model-delegation)
  * [Translation tool](/home/translation-tool)
</Note>

### Response format

For non-streaming responses, the search results and answer are in the `choices[0].message.content` field. For streaming responses, the search results and answer are in the `choices[0].delta.content` field.

The response also contains a `web_search_data` field that contains the following information:

| Parameter               | Type     | Description                                    |
| ----------------------- | -------- | ---------------------------------------------- |
| `query`                 | `string` | The search query that was submitted            |
| `answer`                | `string` | Generated answer based on the search results   |
| `sources`               | `array`  | The search results found                       |
| `sources[].url`         | `string` | URL of the search result                       |
| `sources[].raw_content` | `string` | Raw content from the source URL (if requested) |

<Warning>
  You can use the web search tool with streaming chat responses, but the web search tool call is not streamed. The sources comes back in the `choices[0].delta.content` field once the full search is complete. Then, the response from the model is streamed.

  If you are unfamiliar with the chat completions endpoint or streaming vs. non-streaming responses, learn more in the [chat completion guide](/home/chat-completion).
</Warning>

See the [chat completion endpoint](/api-reference/completion-api/chat-completion#response-id) for more information on the response fields.

<CodeGroup>
  ```json non-streaming response [expandable] theme={null}
  {
    "id": "2ac04514-35cd-49e7-8d11-df0ace4336db",
    "object": "chat.completion",
    "choices": [
      {
        "index": 0,
        "finish_reason": "tool_calls",
        "message": {
          "content": "The latest developments in AI technology are happening at a rapid pace...",
          "role": "assistant",
          "tool_calls": null,
          "graph_data": {
            "sources": null,
            "status": null,
            "subqueries": null
          },
          "llm_data": null,
          "image_data": null,
          "translation_data": null,
          "web_search_data": {
            "search_depth": "advanced",
            "sources": [
              {
                "url": "https://en.wikipedia.org/wiki/AI_boom",
                "raw_content": null
              },
              {
                "url": "https://techcrunch.com/2025/05/30/its-not-your-imagination-ai-is-speeding-up-the-pace-of-change/",
                "raw_content": null
              },
              {
                "url": "https://techcrunch.com/category/artificial-intelligence/",
                "raw_content": null
              },
              {
                "url": "https://techcrunch.com/2025/08/03/inside-openais-quest-to-make-ai-do-anything-for-you/",
                "raw_content": null
              },
              {
                "url": "https://en.wikipedia.org/wiki/Artificial_intelligence",
                "raw_content": null
              }
            ]
          },
          "refusal": null
        },
        "logprobs": null,
        "trace_details": null
      }
    ],
    "created": 1754521236,
    "model": "palmyra-x5",
    "usage": {
      "prompt_tokens": 3736,
      "total_tokens": 3764,
      "completion_tokens": 28,
      "completion_tokens_details": null,
      "prompt_token_details": null
    },
    "system_fingerprint": "v1",
    "service_tier": null
  }
  ```

  ```json streaming response [expandable] theme={null}
  {
    "id": "94131ba5-35f0-4319-b4a6-3929ebb1669a",
    "choices": [
      {
        "delta": {
          "content": "",
          "graph_data": {
            "sources": null,
            "status": null,
            "subqueries": null
          },
          "llm_data": null,
          "refusal": null,
          "role": "assistant",
          "tool_calls": null,
          "translation_data": null,
          "image_data": null,
          "web_search_data": {
            "search_depth": "advanced",
            "sources": [
              {
                "url": "https://en.wikipedia.org/wiki/AI_boom",
                "raw_content": null
              },
              {
                "url": "https://techcrunch.com/2025/05/12/improvements-in-reasoning-ai-models-may-slow-down-soon-analysis-finds/",
                "raw_content": null
              },
              {
                "url": "https://techcrunch.com/category/artificial-intelligence/",
                "raw_content": null
              },
              {
                "url": "https://en.wikipedia.org/wiki/Progress_in_artificial_intelligence",
                "raw_content": null
              }
            ]
          }
        },
        "finish_reason": null,
        "index": 0,
        "logprobs": null,
        "message": {
          "content": "",
          "refusal": null,
          "role": "assistant",
          "graph_data": {
            "sources": null,
            "status": null,
            "subqueries": null
          },
          "llm_data": null,
          "tool_calls": null,
          "translation_data": null,
          "image_data": null,
          "web_search_data": {
            "search_depth": "advanced",
            "sources": [
              {
                "url": "https://en.wikipedia.org/wiki/AI_boom",
                "raw_content": null
              },
              {
                "url": "https://techcrunch.com/2025/05/12/improvements-in-reasoning-ai-models-may-slow-down-soon-analysis-finds/",
                "raw_content": null
              },
              {
                "url": "https://techcrunch.com/category/artificial-intelligence/",
                "raw_content": null
              },
              {
                "url": "https://en.wikipedia.org/wiki/Progress_in_artificial_intelligence",
                "raw_content": null
              }
            ]
          }
        },
        "trace_details": null
      }
    ],
    "created": 1754521528,
    "model": "palmyra-x5",
    "object": "chat.completion.chunk",
    "service_tier": null,
    "system_fingerprint": "v1",
    "usage": null
  }
  ```
</CodeGroup>

## Usage example

This example uses the web search tool to find current information about AI developments during a chat completion.

To use the web search tool:

1. Create a `tools` array that specifies the web search tool.

* The tool array should include the `type` and `function` parameters. The `function` parameter should include the `include_domains` and `exclude_domains` parameters.

2. Create a `messages` array that contains the user message that prompts the model to use the web search tool.

* The message should contain the requirements for the search, such as including raw source text, limiting the number of sources, or specifying time ranges.

3. Call the `chat.chat` method with the `tools` parameter set to the `tools` array and `tool_choice` set to `auto`.

<CodeGroup>
  ```bash cURL theme={null}
  curl --location 'https://api.writer.com/v1/chat' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $WRITER_API_KEY" \
      --data '{
          "model": "palmyra-x5",
          "messages": [
              {
                  "role": "user",
                  "content": "What are the latest developments in AI technology?"
              }
          ],
          "tool_choice": "auto",
          "tools": [
              {
                  "type": "web_search",
                  "function": {
                      "include_domains": ["wikipedia.org", "github.com", "techcrunch.com"],
                      "exclude_domains": ["quora.com"]
                  }
              }
          ]
      }'
  ```

  ```python Python theme={null}
  from writerai import Writer

  # Initialize the Writer client. If you don't pass the `api_key` parameter,
  # the client looks for the `WRITER_API_KEY` environment variable.
  client = Writer()

  messages = [{"role": "user", "content": "What are the latest developments in AI technology?"}]

  response = client.chat.chat(
      model="palmyra-x5", 
      messages=messages, 
      tools=tools,  # The tools array defined earlier.
      tool_choice="auto"
  )

  print(response.choices[0].message.content)
  ```

  ```js JavaScript theme={null}
  import { Writer } from "writer-sdk";

  // Initialize the Writer client. If you don't pass the `apiKey` parameter,
  // the client looks for the `WRITER_API_KEY` environment variable.
  const client = new Writer();

  const messages = [{role: "user", content: "What are the latest developments in AI technology?"}];

  const response = await client.chat.chat({
      model: "palmyra-x5", 
      messages: messages, 
      tools: tools, // The tools array defined earlier.
      tool_choice: "auto"
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

If you want to verify the web search data in the response, you can print the `web_search_data` field to see the search query, answer, and sources that the web search tool used.

<CodeGroup>
  ```python Python theme={null}
  print(response.choices[0].message.web_search_data)
  ```

  ```js JavaScript theme={null}
  console.log(response.choices[0].message.web_search_data);
  ```
</CodeGroup>

By following this guide, you can use the web search tool to have the model search the web for current information during a chat completion.

## Next steps

Learn more about the prebuilt tools available for chat completions:

* [Knowledge Graph tool](/home/kg-chat)
* [LLM tool](/home/model-delegation)
* [Translation tool](/home/translation-tool)


# Chat
Source: https://dev.writer.com/no-code/chat



Agents with the chat capability can answer questions with tailored responses based on topics, personas, or data. You can connect the chat agent to a [Knowledge Graph](/home/knowledge-graph-concepts) to answer questions about your company's data.

<img alt="Agent with chat capabilities" />

## Building an agent with chat

To build an agent with chat capabilities, choose the **Chat** option after clicking **Build an agent** in AI Studio.

<img alt="Create an agent with chat" />

## Write a welcome message

Next, create a welcome message. A welcome message is the first message your users see. Greet them, tell them how to use the chat, and include any special instructions or anything else they should know.

<img alt="welcome-message" />

## Create an avatar

Choose an avatar for your chat. This can be your company logo or any other image you prefer.

<img alt="Creating a chat avatar" />

<Info> Currently only .svg files are supported.</Info>

## Choose a mode

To choose the right mode for your chat, decide how your users should interact with the agent. If you want users to ask general questions, choose the **General chat mode**. If you want users to ask specific questions about your company's data, choose the **Knowledge Graph mode**. If you want users to ask questions about specific documents they provide, choose the **Document mode**. You can also select multiple modes for your agent.

<Tip>
  [Knowledge Graph](https://support.writer.com/category/241-knowledge-graph) lets you ask questions about your company's data and get reliable, accurate answers. It uses multiple internal sources, making it easy to find information even if you're not sure where it's stored. Read more [here](https://support.writer.com/article/244-how-to-use-knowledge-graph).
</Tip>

The following modes are available:

* **General chat mode**: Use this mode to ideate or create content
* **Knowledge Graph mode**: Use this mode to get answers from your company's data stored in a [Knowledge Graph](/home/knowledge-graph-concepts). Before you can use Knowledge Graph mode, you need to [set up a Knowledge Graph](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph) with all the relevant information for your use case. For more granular control over how the agent answers questions using Knowledge Graph, see [Configure Knowledge Graph settings](#configure-knowledge-graph-settings).
* **Document mode**: Use this mode to answer questions about specific documents users provide. You can specify the number, types, and sources of documents you want to allow users to upload. You can also choose to allow users to add URLs or copy and paste text.

<img alt="Choosing a mode for your agent with chat capabilities" />

## Provide instructions

All of the modes allow you to provide instructions. Instructions are the system messages that reach the LLM to provide context or structure around how your chat responds to requests.

<img alt="Providing instructions" />

For example, you can use instructions to:

1. Request answers in a specific language by providing examples.
2. Patch stale data in your Knowledge Graph that's hard to retrieve by providing additional information.
3. Provide context about the users and how to address them.
4. Set limits on the topics that the agent can answer.

Your instructions tell the LLM what answers to generate and how to format them.

## Configure Knowledge Graph settings

When you enable Knowledge Graph mode for a chat agent, you can access advanced configuration options to fine-tune how the agent searches, ranks, and retrieves content from your Knowledge Graph. These settings control the balance between keyword and semantic search, how closely responses match source material, relevance thresholds, and response length.

<img alt="Knowledge Graph configuration interface" />

### Access configuration options

To configure Knowledge Graph settings for your chat agent:

1. **Enable Knowledge Graph mode**: In your chat agent configuration, turn on the **Knowledge Graph mode** toggle
2. **Open configuration**: Click the configuration slider in the top right corner of the Knowledge Graph mode box when Knowledge Graph mode is enabled
3. **Adjust settings**: Use the sliders and options in the configuration panel to customize behavior

<img alt="Graph configuration options" />

### Configuration parameters

#### Grounding level

Controls how closely responses must match source material. This setting determines how much creative interpretation the AI can apply to Knowledge Graph content.

**How it works**:

* **Lower values (closer to 0)**: Grounded outputs - stick closely to source material with minimal creativity
* **Higher values (closer to 1)**: Higher creativity - allow more creative interpretation of source material

**Examples**:

* `0.0`: "According to the documentation, the API supports JSON responses" (direct quote/paraphrase)
* `0.5`: "The API documentation indicates that JSON responses are supported, which suggests additional capabilities" (interpretive)
* `1.0`: "Based on the available information, users can expect JSON responses, though other formats might be possible" (highly interpretive)

**When to adjust**:

* **Increase** for higher creativity when you want more interpretive responses
* **Decrease** for grounded outputs when factual reporting or accuracy is critical

#### Top-k

Controls the balance between keyword and semantic search in ranking results. This parameter determines how the system prioritizes different types of matching.

**How it works**:

* **Higher values (closer to 100)**: Prioritize keyword-based matching
* **Lower values (closer to 0)**: Prioritize semantic similarity matching

**When to adjust**:

* **Increase** for searches where exact keyword matches matter most
* **Decrease** for searches where conceptual similarity is more important

#### Max tokens

Maximum number of tokens the model can generate in the response. This controls the length of the AI's answer.

**How it works**:

* **Higher values**: Allow longer, more detailed responses
* **Lower values**: Generate shorter, more concise responses

**When to adjust**:

* **Increase** for detailed analysis or comprehensive answers
* **Decrease** for quick summaries or when you need faster responses

#### Sub-question count

Maximum number of sub-questions to generate when processing complex queries. Higher values allow the system to break down complex questions into more detailed sub-queries.

**How it works**:

* **Higher values**: Improve detail by generating more sub-questions for thorough analysis
* **Lower values**: Reduce response time by generating fewer sub-questions

**When to adjust**:

* **Increase** for complex, multi-part questions that need thorough analysis
* **Decrease** for simple, direct questions to reduce processing time

#### Weight distribution

Controls how many text snippets to retrieve from the Knowledge Graph for context. This works together with the Top-k setting to control best matches vs broader coverage.

**How it works**:

* **Lower values (5-15)**: Best matches - retrieve fewer, more relevant snippets
* **Higher values (15-25)**: Broader coverage - retrieve more snippets for comprehensive context
* **Values below 5**: May return no results due to RAG implementation limitations

**Important notes**:

* **Recommended range**: 5-25 (default is 30, which is higher than recommended)
* Due to RAG system behavior, you may see more snippets than requested

**When to adjust**:

* **Increase** for broader coverage when you need comprehensive research or more context
* **Decrease** for best matches when you want focused queries or concise results

#### Keyword relevance threshold

Controls how closely keyword matches must align with your query. Higher settings require closer matches, while lower settings allow broader keyword relevance in results.

**How it works**:

* **Low**: Broader range - includes results with looser keyword matches
* **Medium**: Balanced filtering for most use cases
* **High**: Stricter relevance - only includes results with strong keyword matches

**When to adjust**:

* **Set higher** for stricter relevance when you need precise keyword matching
* **Set lower** for broader range when you want to capture more potential matches

#### Semantic relevance threshold

Controls how closely results must match the meaning of your query. Higher settings require stronger semantic similarity, while lower settings allow varied interpretations.

**How it works**:

* **Low**: Broader range - includes results with looser semantic connections
* **Medium**: Balanced filtering for most use cases
* **High**: Stricter relevance - only includes results with strong semantic similarity

**When to adjust**:

* **Set higher** for stricter relevance when you need precise conceptual matching
* **Set lower** for broader range when you want to capture more contextually related content

### Common configuration patterns

The default configuration for Knowledge Graph mode handles most use cases. You can adjust the configuration to your specific needs. Below are some common configuration patterns for different use cases.

#### Research and analysis

Use this configuration for comprehensive research tasks where you need thorough analysis. Higher sub-questions and broader coverage provide more context.

**Settings**:

* **Grounding level**: 0.1 (very grounded)
* **Top-k**: 60 (balanced search)
* **Max tokens**: 7000 (detailed responses)
* **Sub-question count**: 8 (thorough analysis)
* **Weight distribution**: 25 (broad coverage)
* **Keyword relevance threshold**: Low (broader range)
* **Semantic relevance threshold**: Low (broader range)

#### Quick answers

Use this configuration for fast, focused responses where speed and precision matter more than comprehensive analysis.

**Settings**:

* **Grounding level**: 0.0 (completely grounded)
* **Top-k**: 80 (keyword-focused)
* **Max tokens**: 2000 (concise responses)
* **Sub-question count**: 3 (quick processing)
* **Weight distribution**: 15 (focused results)
* **Keyword relevance threshold**: High (stricter relevance)
* **Semantic relevance threshold**: High (stricter relevance)

#### Creative content generation

Use this configuration when you want the AI to interpret and build upon source material creatively.

**Settings**:

* **Grounding level**: 0.6 (interpretive)
* **Top-k**: 40 (semantic-focused)
* **Max tokens**: 5000 (moderate length)
* **Sub-question count**: 6 (balanced analysis)
* **Weight distribution**: 30 (comprehensive context)
* **Keyword relevance threshold**: Medium (balanced filtering)
* **Semantic relevance threshold**: Low (broader range)

### How parameters work together

Some parameters interact in ways that affect both results and performance:

* **Weight distribution and Top-k**: Work together to control best matches vs broader coverage
* **Weight distribution and Max tokens**: Weight distribution controls input context size, while Max tokens controls output response length
* **Grounding level and Sub-question count**: Both affect how thoroughly the system processes queries

### Performance considerations

* **Higher Max tokens**: Increases processing time and cost
* **Higher Sub-question count**: Increases processing time for complex queries
* **Lower Weight distribution**: May return fewer results but with higher relevance
* **Higher Grounding level**: May require more processing to balance creativity with accuracy

### Best practices

1. **Start with defaults**: Begin with the default configuration and adjust based on your specific needs
2. **Test incrementally**: Change one parameter at a time to understand its effect
3. **Consider your use case**: Different applications, like research, Q\&A, and content generation, benefit from different configurations
4. **Monitor performance**: Track how different configurations affect response quality and processing time
5. **Balance precision and recall**: Higher Top-k values give more precise results but may miss relevant content

### Apply default settings

If you've made changes and want to return to the recommended default configuration, click **Apply default settings** in the configuration panel. This resets all parameters to their optimal values for general Knowledge Graph queries.


# Chat with Knowledge Graph
Source: https://dev.writer.com/no-code/chat-kg



With Knowledge Graph mode for chat agents, you can create agents that can search for information and answer questions specific to your company's data.

This guide walks through the steps to create a Knowledge Graph from a Confluence space. Then, it creates a chat agent that uses that Knowledge Graph to answer questions about your company's policies.

<img alt="Example of a chat agent with Knowledge Graph" />

<Steps>
  <Step title="Create a Knowledge Graph">
    To get started, set up a Knowledge Graph in Writer.

    From the home screen of AI Studio, click **Knowledge Graphs** in the left sidebar and click **Add a Graph**.

    <img alt="Create a Knowledge Graph" />

    <Warning>Knowledge graphs created in AI Studio aren't accessible in the Ask Writer core app unless you define the Knowledge Graph as part of a custom agent.</Warning>
  </Step>

  <Step title="Add graph name and description">
    In the modal that appears, give your graph a name and description.

    <img alt="Add a graph" />
  </Step>

  <Step title="Add data to your graph">
    Choose how you want to add your data to the graph and add your data:

    * Add via manual file upload: Upload files manually from your device
    * Add via data connector: Integrate data from an existing tool such as Confluence, Notion, and Google Drive
    * Add via API: Send docs to Writer [via the API](/home/knowledge-graph)
    * Add websites: Create a list of websites to search

    This example uses the **Add via data connector** option to integrate data from a Confluence space. To use this option, you need to have access to the Confluence space you want to integrate.

    Learn more about creating and managing a Knowledge Graph in the [Knowledge Graphs support article](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph).
  </Step>

  <Step title="Create an agent with chat capabilities">
    Once you create your Knowledge Graph, you can then create an agent with chat capabilities and add the Knowledge Graph to the agent.

    From the [AI Studio home page](https://app.writer.com/ai-studio), click **Build an agent** in the top right corner. Then, select **Chat** as the type of agent you want to create.
  </Step>

  <Step title="Enable the Knowledge Graph mode">
    For this example, enable only the **Knowledge Graph mode** so the chat agent answers questions specifically about your company's data from the Knowledge Graph.

    In the agent configuration, turn off the **General chat mode** toggle and turn on the **Knowledge Graph mode**.

    Next, select **Always stay connected to a specific set of graphs** and select the Knowledge Graph you created in the previous step. This ensures that the agent only answers questions about the data in the Knowledge Graph you provide, and doesn't allow the user to switch to a different Knowledge Graph.

    <img alt="Enable Knowledge Graph" />

    <Tip>
      **Want to further customize Knowledge Graph behavior?** You can fine-tune how your agent searches, ranks, and retrieves content from your Knowledge Graph using advanced configuration options. Learn more about [configuring Knowledge Graph settings](/no-code/chat#configure-knowledge-graph-settings) for detailed control over grounding level, search balance, response length, and more.
    </Tip>
  </Step>

  <Step title="Add instructions for the model">
    You can add additional instructions for Knowledge Graph mode by clicking **Add instructions**.

    The instructions can include context for the model to understand what this agent does, and what types of answers it should provide. You may also want to include instructions on what to output if someone asks a question that isn't specific to a customer story or quote.

    For example, you could include: <PromptComponent />
  </Step>

  <Step title="Test your agent">
    On the right side of the screen, you can see what your agent looks like and test it.

    Try asking a variety of questions to make sure you're getting the types of answers you expect. You can continue to update the instructions until you're happy with the answers you receive.
  </Step>

  <Step title="Deploy your agent">
    Once you're happy with how everything looks, [deploy your agent](/no-code/deploying-an-agent) so that everyone can use it.
  </Step>
</Steps>


# Choose your use case
Source: https://dev.writer.com/no-code/choosing



With our no-code tools, you can build agents for these use cases:

1. **Chat:** Answer questions with tailored responses based on topics, personas, or data.
2. **Text generation:** Collect specific user inputs and generate customized outputs. Best for long- or short-form structured content, such as blog posts, FAQs, press releases, and newsletters.
3. **Research:** Conduct web research and receive a structured report.

Define the structure of the agent by outlining the inputs (the information you provide) and the outputs (the content you receive).

Here are some input and output examples:

## Chat

**Inputs:**

* User queries about specific topics (for example, "What are your store hours?")
* User requests for assistance (for example, "Help me track my order.")

**Outputs:**

* Direct answers to user queries (for example, "Our store hours are 9 AM to 9 PM, Monday through Friday.")
* Assistance responses and guidance (for example, "I can help you track your order. Please provide your order ID.")

## Text generation

**Inputs:**

* Content briefs or outlines that specify the topic you want to write about, the SEO keywords and a CTA you want to include.

**Outputs:**

* Completed text that includes SEO keywords and the specific CTA

## Research

**Inputs:**

* Questions or briefs about the topic that you want to research.
* Whether the research should perform a general search of the web or limit the search to specific sources.

**Outputs:**

* A summary of the information found, including links to the sources.


# Deploy an agent
Source: https://dev.writer.com/no-code/deploying-an-agent



<Tip> Full-access users can deploy any agent in their organization. Individual builders can deploy agents they create.</Tip>
To deploy a no-code agent, first click on the **Deploy** tab at the top of the page. Depending on your organization's status, you'll have up to four deployment options:

* [**Playground**](#playground): Receive a URL to share with others to test the agent.
* [**Embed agent**](#embed-agent): Embed the agent on your own website or portal.
* [**Deploy to Slack**](#deploy-to-slack): Enable the agent for use within Slack.
* [**Deploy to Writer**](#deploy-to-writer): Enable the agent for use within Writer.

## Playground

Playground is the easiest way to share and test your agent. Just go to **Deploy** and turn on the Playground toggle. You can either copy the Playground URL to share with others, or open the agent in **Playground** in a new tab.

<img alt="Deploying an agent to the playground" />

The **Open** button will take you directly to this agent through the Playground, while the **Copy link** option will generate a shareable link that you can send to any tester or end user to get feedback on your agent.

<Warning>
  Links to the Playground view of an agent don't require authentication. This can be useful for providing external users access to test an agent (they won't be able to navigate elsewhere within your AI Studio).
</Warning>

## Embed agent

Embedding an agent allows you to embed the agent on your own website or portal.

<Steps>
  <Step title="Toggle embed option">
    First, go to the **Deploy** tab and toggle the **Embed Agent** option.

    <img alt="Embedding an agent" />
  </Step>

  <Step title="Add domains to allowlist">
    Prevent unauthorized use of this agent by adding domains to the allowlist

    <img alt="Add domains to allowlist" />
  </Step>

  <Step title="Select embed style">
    After enabling embed, select your preferred style: centered or widget.

    <img alt="Embedding styles" />
  </Step>

  <Step title="Copy the snippet">
    You'll see a block of code that contains the snippet you need to include wherever you want, such as on your own website or portal. Copy the iFrame snippet and share it with your engineering team to embed this agent. You can disable the embed snippet at any time in AI Studio, and any changes you push will immediately update the embeds.

    <img alt="Copy the iFrame snippet" />
  </Step>
</Steps>

## Deploy to Slack

If your organization uses chat agents with General chat mode or Knowledge Graph mode with AI Studio graphs, you'll see a Deploy to Slack option. This allows you to enable the agent for use within Slack. Once deployed, the agent will be available in Slack, and you'll be able to access it directly from within your workspace.

<Warning>Deployed agents in Slack will charge token usage. Please refer to our [pricing page](/home/pricing) for more details.</Warning>
<Note>You'll have access to this deployment option if your organization uses agents with chat capabilities using General chat mode or Knowledge Graph mode.</Note>

<Steps>
  <Step title="Toggle Slack option">
    First, go to the **Deploy** tab and click the **Connect to Slack** link.

    <img alt="Deploying an agent to Slack" />
  </Step>

  <Step title="Go through the OAuth flow">
    Next, you'll need to go through the OAuth flow to allow Writer to access your Slack workspace.

    <img alt="Go through the OAuth flow" />
  </Step>

  <Step title="Toggle Slack option">
    You can now toggle the **Deploy to Slack** option to enable the agent for use within Slack.
  </Step>

  <Step title="Connect to Writer">
    Once the agent is Slack-enabled, you can start interacting with it by clicking the Writer button in Slack and selecting "Connect to Writer."

    <img alt="Connect to Writer" />
  </Step>

  <Step title="Return to Slack">
    When you return to Slack, the agent will be available for use. You'll see that the Q\&A functionality is now integrated within Slack.

    <img alt="Return to Slack" />
  </Step>

  <Step title="Select interaction mode">
    When using the agent in Slack, you'll have two interaction modes:

    * **General mode:** If the agent uses General mode and doesn't include a Knowledge Graph mode, simply select it and start chatting.
    * **Knowledge Graph mode:** If the agent includes a Knowledge Graph, select the interaction mode—either Knowledge Graph mode or General mode.
      * In General mode, it'll function like a non-graph-enabled agent.
      * In Knowledge Graph mode, you'll be prompted to connect the relevant graphs. Once connected, you can start chatting with the agent.

    If the agent is built with specific data constraints (e.g., a sales Q\&A agent linked only to Salesforce data), you'll select the Knowledge Graph, and the setup will be complete without needing to choose different graphs. This deployment mode leverages the configurations set in the agent builder, ensuring the agent behaves as expected in Slack

    <img alt="Select interaction mode" />
  </Step>
</Steps>

## Deploy to Writer

If your organization uses the Writer App in addition to AI Studio, you'll see a **Deploy to Writer** option. This allows you to select specific teams in Writer where you want to deploy the agent.
<Note>You'll have access to this option if you have access to the Writer App.</Note>

<Steps>
  <Step title="Update Agent Info">
    Before deploying to Writer, update the **Agent Info** under the **Agent Guide** tab to provide helpful information about the agent to users.

    <img alt="Agent Info" />
  </Step>

  <Step title="Deploy to Writer">
    Click the deploy button. Once deployed, the agent will be available to those teams within the app library. You can choose to deploy the agent to all teams or select specific teams.

    <img alt="Deploy to Writer" />
  </Step>
</Steps>


# Create an earnings call agent
Source: https://dev.writer.com/no-code/earnings-call-agent



This agent takes information from an earnings call transcript and puts it together in a summary report. The report covers results for the period, future plans, and management opinions. It uses [Palmyra X5](/home/models#palmyra-x5), a Writer model with a large context window to handle the length of earnings call transcripts.

<img alt="Earnings call agent" />

Below are the steps to create this earnings call agent. To follow along, first log in to [AI Studio](https://app.writer.com/aistudio).

<Steps>
  <Step title="Create a new agent with text generation capabilities">
    From the [AI Studio home page](https://app.writer.com/aistudio), click **Build an agent** in the top right corner. Then, select **Text generation** as the type of agent you want to create.
  </Step>

  <Step title="Define your inputs">
    This agent requires the earnings call transcript as input.

    Define one input of type **File upload** with the name: **Earnings Call Transcript**. You can specify whether to allow the user to add a URL, upload a file, or paste text. You can also specify the file type to be uploaded.

    *Optional: You could also add text inputs for company name and reporting period if you want those to be dynamic.*
  </Step>

  <Step title="Add the quarterly results prompt">
    This agent uses multiple prompts to break down the task: one for summarizing results, one for forward-looking guidance, and one for management commentary.

    Add a prompt of type with the name **Quarterly Results** and select **Palmyra X5** as the model.

    Add the following prompt:

    ```
    You are a financial analyst who excels at producing summaries of financial reports.

    Here is your report to analyze:

    <report>@File input</report>

    Instructions:

    - Analyze the dates given in the report to determine what timeframe the report is for
    - Financial reports are often produced on a quarterly basis.
    - Extract key business result here for the quarter the earnings call is for
    - These highlights should be all about the numbers
    - You must pull out as many of these as you can find in the earnings call.
    - You must pull out a minimum of 5 results, but you should write more if there are more key results to share.
    - Copy the most relevant portion of the earnings call for each result you find here. Each result should have a supporting extract copied from the document. Do not modify the extract, just copy it exactly as it appears in the earnings call.
    - Output as HTML instead of Markdown

    Output:
    ```
  </Step>

  <Step title="Add the future guidance prompt">
    Add a prompt of type with the name **Future Guidance** and select **Palmyra X5** as the model.

    Add the following prompt:

    ```
    You are a financial analyst who excels at producing summaries of financial reports.

    Here is your report to analyze:

    <report>@File input</report>

    Instructions:

    - If this report contains information about future quarters:
    - Extract as many statistics from the future quarter guidance as you can
    - There should be at least 5 of these statistics
    - Remember to focus on the numbers
    - Output as HTML, not Markdown

    Output:
    ```
  </Step>

  <Step title="Add the management commentary prompt">
    Add a prompt of type with the name **Management Commentary** and select **Palmyra X5** as the model.

    Add the following prompt:

    ```
    You are a financial analyst who excels at producing summaries of financial reports. The management of the company the earnings call is for will cover a variety of themes during the earnings call. 

    Here is your report to analyze:

    <report>@File input</report>

    Instructions:

    - Pull out a list of at least 5 themes
    - Each theme should have a longer quote from the report that covers what the management of the company has to say about it. These can be quite lengthy and should give detail about the selected theme. Include a quote to go with each theme.
    - Output as HTML, not Markdown

    Output:
    ```
  </Step>

  <Step title="Add the participants prompt">
    Add a prompt of type with the name **Participants** and select **Palmyra X5** as the model.

    Add the following prompt:

    ```
    You are a financial analyst who excels at producing summaries of financial reports. Your specific goal is to extract any and all participants from financial call transcripts or reports. Include any names and titles of participants.

    Here is your report to analyze:

    <report>@File input</report>

    Instructions:

    - Pull out all participants from the report
    - Include all participants as a bulleted list
    - If included, share the title/job name of each participant
    - Output as HTML, not Markdown

    Output:
    ```
  </Step>

  <Step title="Format your output">
    Add the following to the **Output formatting** section:

    ```
    <h3>Participants:</h3>
    @Participants
    <h3>Quarterly Report:</h3>
    @Quarterly Results
    <h3>Future Guidance:</h3>
    @Future Guidance
    <h3>Management Commentary:</h3>
    @Management
    ```
  </Step>

  <Step title="Test and refine your output">
    Review the generated summary. If it's not capturing the right information or the tone is off, tweak your prompts. Consider adding more specific instructions or examples to guide the model.
  </Step>

  <Step title="Deploy your agent">
    Once you're happy with the summary quality, [deploy your agent](/no-code/deploying-an-agent) so others can use it. Developers can also [invoke this agent with the API](/home/applications) and [use it with tool calling](/home/applications-tool-calling) to integrate into other agentic workflows.
  </Step>
</Steps>


# Edit an agent
Source: https://dev.writer.com/no-code/editing-an-agent



You can edit a deployed agent's name, configuration, and metadata (in Agent Guide) and then push the changes to the deployed instances of the agent. To edit your deployed agent:

<Steps>
  <Step title="Click Unlock to edit">
    Click **Unlock to edit** to change either the agent configuration or the agent itself.

    <img alt="Editing an agent" />
  </Step>

  <Step title="Edit the agent">
    Edit the agent's name, configuration, or metadata (in Agent Guide).

    <img alt="Editing an agent" />
  </Step>

  <Step title="Push name or metadata changes only">
    If you've only edited the agent's name or metadata, click the down arrow and select **Push configuration changes only** or **Push guide changes only**.

    <img alt="Push name or metadata changes" />
  </Step>

  <Step title="Push all changes">
    If you want to push all changes, click **Push all changes** to update the deployed instances of your agent.

    <img alt="Editing an agent" />
  </Step>
</Steps>


# No-code agents
Source: https://dev.writer.com/no-code/introduction

Build AI agents without code to automate tasks, create branded content, and develop knowledge assistants using your company data.

No-code tools let you build agents **without writing any code** to compress your workflows—automate repetitive tasks, create branded content, and develop knowledge assistants based on your company's data. Simply define the required inputs, the instructions for the agent to follow, and the desired output structure. Once you've built the agent, deploy it and edit it at any time.

<img alt="Agent capabilities" />

You can build no-code agents for:

1. **Recurring** **tasks** that require repetitive processing, such as creating
   recurring content.
2. **Time-intensive tasks** that require extensive back-and-forth communication between
   contributors to complete.
3. **Structured** **tasks** that require the same information
   to be requested and always produce the same result.
4. **Voice-specific** **tasks** that require a particular tone, style, or format.

<Tip>Explore the pre-built, no-code agents in [Templates](https://app.writer.com/aistudio/templates). When you find one
you like, click "Try in builder" to copy and test it. If it meets your needs,
you can deploy it immediately and edit it after deployment.</Tip>


# Create a newsletter generation agent
Source: https://dev.writer.com/no-code/newsletter-agent



Newsletters need to easy-to-understand and jargon-free, and can take hours to write—especially if you're sending them weekly. Creating an agent can help you get these newsletters out faster and written in a way that your customers can understand.

<img alt="newsletter agent showing inputs, prompts, and the generated newsletter content preview." />

Below are the steps to create this newsletter agent. To follow along, first log in to [AI Studio](https://app.writer.com/aistudio).

<Steps>
  <Step title="Create a new agent with text generation capabilities">
    From the [AI Studio home page](https://app.writer.com/aistudio), click **Build an agent** in the top right corner. Then, select **Text generation** as the type of agent you want to create.
  </Step>

  <Step title="Define your inputs">
    This example is a newsletter that always includes three key components:

    * Communication about new features, products, or white papers
    * A customer spotlight
    * Information about the latest blog post

    Each of these components is an input. Define three inputs, each of the type **File upload**, with the names:

    * Feature info/whitepapers
    * Customer Spotlight
    * Blog to promote
  </Step>

  <Step title="Write your prompts">
    There are several ways to break up the prompts for generating a newsletter. In this example, create two prompts: one to generate the subject line and one to generate the body of the newsletter.

    The prompt for the subject line is:

    > Create a subject line for a weekly newsletter about the new features, customer spotlights, and featured blog posts.

    The prompt for the body of the newsletter is:

    > Create a newsletter for Writer customers. The newsletter should include:
    >
    > * communication about new features, products, or white papers. These items to highlight are in `@Feature info/whitepapers`
    > * a customer spotlight. The customer spotlight information is in `@Customer Spotlight`
    > * a blog article to promote, which is in `@Blog to promote`
    >
    > Avoid jargon and complex language. Although the newsletter may cover fairly complex topics, it should be accessible to everyone.
    > The newsletter should follow this format:
    >
    > * a greeting and introduction
    > * new features/products/whitepaper communications
    > * the customer spotlight
    > * blog article to promote
    >
    > Include suggestions for images that can break up each section.
  </Step>

  <Step title="Format your output">
    Decide how you want your output to look in the Output formatting section. You can use [Markdown](https://www.markdownguide.org/) to format your output.

    In this example, the output is a newsletter with a subject line and body:

    > @subject\_line
    >
    > @body
  </Step>

  <Step title="Test and refine your output">
    The first version of your newsletter may not be what you're looking for. That's okay! The more you tweak your prompts and provide examples, the better your output will be.

    It can also be helpful to provide examples in your prompts of newsletters and subject lines you think are the best. That way, Writer can learn from what you consider to be the best newsletters.
  </Step>

  <Step title="Deploy your agent">
    Once you're happy with how everything looks, [deploy your agent](/no-code/deploying-an-agent) so that everyone can use it. Developers can also [invoke this agent with the API](/home/applications) and [use it with tool calling](/home/applications-tool-calling) to integrate into other agentic workflows.
  </Step>
</Steps>


# Research
Source: https://dev.writer.com/no-code/research



A no-code agent with the research capability can perform web research on a specific topic. You can use the pre-built research type or create a custom one that can be tailored to a specific topic and writing style.

<img alt="Research screen" />

## Building an agent with research capabilities

To build an agent with research capabilities, first choose the **Research assistant** option after clicking **Build an agent** in AI Studio.

<img alt="Create a research agent" />

## Selecting the type of research

The first step is to select the type of research from the **Research** menu to incorporate into your agent. You have the option of choosing from a selection of pre-built research that specialize in popular topics, or creating a custom one that can be tailored to a specific topic and writing style.

<img alt="&#x22;Research&#x22; drop-down menu displaying all options" />

### Pre-built research

The **Research** menu starts with this selection of pre-built research:

* **Auto research**: automatically selects the most appropriate research type based on the topic
* **Finance research**: use this research for topics related to finance and investing
* **Business research**: helps with business-related topics
* **Travel research**: for topics about travel and tourism, use this research

### Custom research

**Custom research**, the final option in the **Research** menu, allows you to create research tailored to your specific needs. When you select this option, the **Edit instructions** link will appear.

<img alt="&#x22;Research&#x22; menu, with &#x22;Custom research&#x22; selected and Edit instructions&#x22; link" />

Click this link to open the **Custom instructions** window, where you can specify how the research should behave, what topics it should cover, and how it should respond to user queries.

<img alt="&#x22;Custom instructions&#x22; window" />

## Giving the research search parameters

The **Search type** options allow you to specify how the research should search for information.

<img alt="&#x22;Search type&#x22; menu, with &#x22;Web search&#x22; selected" />

You can choose from the following:

* **Web search**: The research will search the web for information.
* **Specific URLs**: The research will limit its search to the web sites or pages you specify. When you select this option, a text field will appear where you can enter one or more URLs of the sources you want the research to search. Note that the URLs you provide must start with `http://` or `https://`.

<img alt="Search type menu, with &#x22;Specific URLs&#x22; selected, and text field for entering URLs" />


# Text generation
Source: https://dev.writer.com/no-code/text-generation



Agents with the text generation capability can generate text based on specific inputs. These no-code agents are ideal for long- or short-form structured content, such as blog posts, FAQs, press releases, and newsletters.

<img alt="Agent with text generation" />

## Building an agent with text generation

To build an agent with text generation capabilities, choose the **Text generation** option after clicking **Build an agent** in AI Studio.

<img alt="Create a text generation agent" />

An agent with text generation capabilities consists of:

* [Adding inputs](/no-code/text-generation#inputs)
* [Writing prompts](/no-code/text-generation#prompts)
* [Formatting outputs](/no-code/text-generation#output-formatting)
* [Choosing a voice](/no-code/text-generation#voice)

## Inputs

An input is content information for your text generation. It generates the desired output—the content your text generation will produce.

For example, if you're building a blog post, the input would include the blog post topic, outline, keywords, reference materials, research sources, and existing knowledge about the topic.

### Types of inputs

There are four types of inputs:

1. Text input: A text input allows users to enter text directly into the text generation.
2. Dropdown: A dropdown input allows users to select one option from a list.
3. File upload: A file upload input allows users to upload a file, such as an image or PDF.
4. Image upload: An image upload input allows users to drag-and-drop or choose an image.

### Adding an input

Once you've decided what type of input you want to add, you'll need to:

<Steps>
  <Step title="Add an input">
    Click **Add input** and select the type of input you want to add.

    <img alt="Adding an input" />
  </Step>

  <Step title="Name your input">
    Give your input a name that clearly communicates its purpose to users. This
    will help them understand what information they need to provide.

    <img alt="Adding an input" />
  </Step>

  <Step title="Decide whether the input is required or optional">
    If it's required, users must enter information in that field in order to use
    the text generation. If it's optional, they can skip it.
  </Step>

  <Step title="Add placeholder text or descriptions">
    Provide placeholder text or descriptions to clarify what information is required. This ensures that users fully understand what they need to enter.
  </Step>

  <Step title="Control what users can upload">
    If you selected a file upload input, you can restrict the type of file users can upload. For example, if your agent expects a specific type of an .XLSX file, restrict other file types from being allowed. This will guide users to use the agent as intended.
  </Step>
</Steps>

## Prompts

A prompt is a message that you send to the model. It can be a single prompt consisting of a few lines, or a complex prompt consisting of multiple steps—each with its own prompt that relies on multiple inputs. You can even use prompting techniques such as chain-of-thought (CoT) prompting, where one prompt output becomes an input to the next prompt, and so on—creating a chain of thought.

<Tip>Check out our [prompting guide](/home/prompting) for strategies on how to write effective prompts.</Tip>

### Adding prompts

You can add multiple prompts to your text generation.

<Steps>
  <Step title="Add a prompt">
    Click **Add prompt** and select the type of prompt you want to add.

    <img alt="Adding a prompt" />
  </Step>

  <Step title="Choose a model">
    Select the [model](/home/models) you want to use for your prompt, as well as any parameters like temperature, top P, and max length.

    <img alt="Choosing a model" />
  </Step>

  <Step title="Write your prompt">
    Write your prompt using the guidelines below and our [prompting guide](/home/prompting).
  </Step>
</Steps>

### How to use prompts effectively

You can ask the model to perform various tasks, but it's best to break down your prompts into smaller, more specific "asks." This is especially important for complex, multi-step text generation. Otherwise, you risk overwhelming the model by asking it to perform too many tasks at once.

For example, instead of asking it to create an entire blog post at once, you might write one prompt asking it to create a title, another to create an opening paragraph, and so on.

Follow these [strategies](/home/prompting) to write good prompts.

<img alt="post-version-a" />

### Renaming prompts

You can rename prompts from generic names like "Prompt 1" and "Prompt 2" to any custom name.

This will help you to:

1. Stay organized and keep track of your prompts
2. Know exactly what each prompt accomplishes
3. Write your prompts
4. Format your output

<img alt="Renaming prompts" />

### Referencing inputs

To reference your input, `@` it within the prompt.

For example, if you're writing a prompt to generate social posts based on a blog post, do the following:

1. **Input:** `Blog post`
2. **Prompt 1:** instructions for creating a social post

In your prompt, reference `@Blog post` so that Writer understands that the social post should be based on the blog input.

<img alt="Reference an input" />

<Tip>
  If you have more than one input, you can `@` each one within a single
  prompt.
</Tip>

### Prompt chaining

You can reference other prompts within a prompt by using the `@` symbol, a feature known as prompt chaining.

For example, if you have a long blog post, you might want to summarize it in your first prompt, and use that summary to generate a social media post in your second prompt:

<Steps>
  <Step title="Input">`Long blog`</Step>

  <Step title="Prompt 1">
    Instructions for generating a summary of the `@Long blog` post.
  </Step>

  <Step title="Prompt 2">
    Instructions for generating a social media post based on `Prompt 1`.
  </Step>
</Steps>

In the second prompt, you'd reference `@Prompt 1` to indicate that you want the social media post to be based on the blog summary.

<img alt="Referencing a prompt" />

## Output formatting

Next, format your output—the final assembled text generation. To do this, take all your prompt outputs (if you have more than one), format their style, add any static text such as a disclaimer, and combine them together.

You can use Markdown or HTML to specify how you'd like your output to be formatted.

For example, you can use the following formatting:

<CardGroup>
  <Card title="Heading Tags" icon="heading">
    Use `<h1>`, `</h1>`, `<h2>`, `</h2>`, and `<h3>`, `</h3>` to denote heading
    text.
  </Card>

  <Card title="Bold Text" icon="bold">
    Use `<strong>` and `</strong>` to denote bolded text.
  </Card>
</CardGroup>

<img alt="Output formatting" />

You don't have to include every prompt in your output formatting. For example, a prompt might be used only to summarize and then to create a LinkedIn post (based on the summary). If you don't want to include the summary in your output, you don't have to.

<Tip>
  Because you don't need to reference every prompt in your final output, you can leave interim prompts in your text generation for A/B testing or to get feedback. Combined with the `Rerun` option, this is a great way to efficiently iterate on your text generation.
</Tip>

The simplest text generation will have only one prompt. A more complex text generation might have multiple section headers, each with its own prompt output.

Before adding even more prompts to your text generation, ask yourself:

1. Does the user want this content?
2. Do I need these inputs to generate this output?

## Voice

With voice calibration, you can create outputs that sound just like you, faster. Voice allows you to build your different voices in Writer, and see them in action across the platform in the content you generate. To learn more about voice calibration, see [how to calibrate voice](https://support.writer.com/article/250-how-to-calibrate-voice-for-your-content).

<Note>
  At this time, only English is supported in Writer voice profiles.
</Note>

For an agent with text generation capabilities, you can choose to:

* Automatically generate all outputs using a voice selected by you
* Allow user to choose which voice to apply to their output
* Generate all outputs using Writer's default voice
* Allow users to choose from any voice in their team

<img alt="Voice" />


# AWS Bedrock
Source: https://dev.writer.com/providers/aws-bedrock

Add AI models from AWS Bedrock to use in AI Studio agents

This guide shows you how to configure AWS Bedrock as an external model provider in AI Studio. After setting up this provider, you can use foundation models from AWS Bedrock when building agents.

<Note>
  AI Studio currently supports Bedrock models in `us-east-1`, `us-west-1`,
  `us-west-2`, and `eu-west-1` regions only. See [Regional
  considerations](#regional-considerations) for details.
</Note>

## Prerequisites

Before adding Bedrock models to AI Studio, you need:

* An [AWS account](https://aws.amazon.com/) with access to Amazon Bedrock
* Model access enabled for the Bedrock models you want to use (see [Enable model access](#enable-model-access-in-bedrock))
* IAM credentials with permission to invoke Bedrock models

<Warning>
  AI Studio supports **text generation** and **embedding** models from Bedrock.
  Video, audio, and image generation models are not supported.
</Warning>

## Configure AWS credentials

AI Studio supports authentication for AWS Bedrock via access keys.

### Access keys

Access keys provide a straightforward way to authenticate with AWS. Create a dedicated IAM user for AI Studio rather than using personal credentials.

#### Create an IAM user

1. Sign in to the [AWS IAM Console](https://console.aws.amazon.com/iam/)
2. Navigate to **Users** in the left sidebar
3. Select **Create user**
4. Enter a username (for example, `writer-aistudio-bedrock`)
5. Select **Next**
6. Choose **Attach policies directly**
7. Search for and select `AmazonBedrockFullAccess`, or create a custom policy with more restrictive permissions (see [IAM policy requirements](#iam-policy-requirements))
8. Select **Next**, then **Create user**

#### Generate access keys

1. Select the user you created
2. Navigate to the **Security credentials** tab
3. Under **Access keys**, select **Create access key**
4. Choose **Third-party service** as the use case
5. Acknowledge the recommendation and select **Next**
6. Add an optional description tag (for example, `AI Studio Bedrock integration`)
7. Select **Create access key**
8. Copy the **Access key ID** and **Secret access key**

<Warning>
  The secret access key is only shown once. Copy it immediately and store it
  securely. If you lose the secret key, you must create new access keys.
</Warning>

## Enable model access in Bedrock

Before using models in AI Studio, you must request access to them in the AWS Bedrock console.

1. Navigate to the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/)
2. Select your preferred region from the region dropdown (for example, **US West (Oregon)**)
3. Navigate to **Model access** in the left sidebar
4. Select **Manage model access**
5. Check the boxes for the models you want to enable
6. Select **Request model access**
7. Wait for the status to change from **In Progress** to **Access granted**

<Note>
  Some models require you to submit a use case and accept additional terms.
  Follow the prompts in the console to complete the access request.
</Note>

For the most current list of available models and regional availability, see the [Amazon Bedrock model documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html).

## Add Bedrock models in AI Studio

After configuring your AWS credentials and enabling model access:

1. Navigate to **Models & Guardrails > Models** in [AI Studio](https://app.writer.com/aistudio)
2. Select **+ Add model**
3. Select **AWS Bedrock** as the provider
4. Enter your access key ID and secret access key
5. Select the AWS region where you enabled model access
6. Select the models you want to add from the available list
7. Configure team access:
   * **All teams**: Anyone with builder access can use the model
   * **Specific teams**: Restrict to selected teams
8. Select **Add model**

<img alt="" />

## Regional considerations

AI Studio supports Bedrock models in the following AWS regions:

* **US East** (`us-east-1`)
* **US West** (`us-west-1`)
* **US West 2** (`us-west-2`)
* **EU West** (`eu-west-1`)

<Note>Support for additional AWS regions is coming soon.</Note>

When configuring Bedrock in AI Studio:

* Select a supported region where you have model access enabled
* Ensure your IAM credentials have permissions in the same region you select
* Some Bedrock models are only available in specific regions—check the [Amazon Bedrock regional availability](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html) documentation

<Warning>
  Ensure the region you select in AI Studio matches the region where you enabled
  model access in AWS. Your IAM user or role must have permissions in that same
  region.
</Warning>

## Monitor costs

AWS bills Bedrock usage directly to your AWS account based on the tokens processed. WRITER also charges a 10% pass-through fee on external model usage. AI Studio tracks usage and costs for external models, providing visibility into spending across all your models in one place.

For information about monitoring model health and automatic recovery, see [Monitor model health](/home/external-models#monitor-model-health).

## Troubleshoot Bedrock configuration

### Invalid credentials error

If you see an "Invalid credentials" or "Authentication failed" error:

* Verify credentials are copied correctly without extra spaces
* Check that access keys are still active in the IAM console
* Ensure the IAM user/role has the required Bedrock permissions

### Model not available error

If a model doesn't appear or returns an error:

* Confirm you've requested and received access to the model in the Bedrock console
* Verify you selected the correct region in AI Studio
* Check that your IAM policy allows access to the specific model

### Access denied error

If you see permission errors when using a model:

* Review your IAM policy to ensure it includes `bedrock:InvokeModel` permission
* Check the policy's `Resource` field allows access to the model you're using

### Unhealthy model status

If a model shows as unhealthy in AI Studio:

* AI Studio automatically retries unhealthy models after a cooldown period
* For transient issues like temporary AWS outages, no action is needed
* For persistent issues, check the troubleshooting items above

## Next steps

* [Add external models](/home/external-models): Learn about managing external models in AI Studio
* [Choose a model](/home/models-overview): Compare Palmyra models with external provider models
* [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/): Explore Bedrock's full capabilities


# Microsoft Azure
Source: https://dev.writer.com/providers/microsoft-azure

Add AI models from Microsoft Azure to use in AI Studio agents

This guide shows you how to configure Microsoft Azure as an external model provider in AI Studio. After setting up this provider, you can use Azure OpenAI models or Foundry Models when building agents.

## Prerequisites

Before adding Microsoft Azure models to AI Studio, you need:

* An [Azure subscription](https://azure.microsoft.com/pricing/purchase-options/azure-account?cid=msft_learn) with access to Azure OpenAI or Foundry Models
* At least one deployed model on your Azure resource (see [setup options](#choose-a-setup-path) below)
* The resource endpoint and API key for the deployment you plan to use

<Warning>
  AI Studio supports **text generation** and **embedding** models from Microsoft
  Azure. Video, audio, and image generation models are not supported.
</Warning>

<Note>
  AI Studio currently supports the following Azure models, with more coming soon:

  * `gpt-4.1`
  * `gpt-4o`
  * `gpt-5`
  * `mistral-large-3`

  To request a model, reach out to your account manager or [support](mailto:support@writer.com).
</Note>

## Choose a setup path

Choose one of the following setup paths depending on how you deploy models in Azure:

* **[Azure OpenAI](#option-1-azure-openai)**: Create an Azure OpenAI resource and deploy models through the Azure portal
* **[Foundry Models](#option-2-foundry-models)**: Deploy models through the Foundry portal

## Option 1: Azure OpenAI

### Create a resource

Create an Azure OpenAI resource in the Azure portal before deploying models.

1. Sign in to the [Azure portal](https://portal.azure.com)
2. Select **Create a resource** and search for **Azure OpenAI**
3. Select **Create**
4. On the **Basics** tab, provide the following:

| Field              | Description                                                                                         |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| **Subscription**   | The Azure subscription to use for your Azure OpenAI resource                                        |
| **Resource group** | The resource group to contain your Azure OpenAI resource. Create a new group or use an existing one |
| **Region**         | The location for your resource. Different regions may introduce latency                             |
| **Name**           | A descriptive name for the resource, such as `writer-aistudio-openai`                               |

5. Select **Next**
6. On the **Network** tab, select **All networks, including the internet, can access this resource** (required for AI Studio to reach the endpoint)
7. Select **Next** to open the **Tags** tab. Add [tags](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources) if your organization requires them
8. Select **Next** to reach **Review + submit**
9. Confirm your settings and select **Create**

For more details, see [Create and deploy an Azure OpenAI resource](https://learn.microsoft.com/en-us/azure/foundry-classic/openai/how-to/create-resource?pivots=web-portal) on Microsoft Learn.

### Deploy a model

Before you can use a model in AI Studio, you need to deploy it on your Azure OpenAI resource.

1. Sign in to [Microsoft Foundry](https://ai.azure.com)
2. Navigate to the Azure OpenAI resource you [created earlier](#create-a-resource)
3. Select **Deployments** from the left pane
4. Select **+ Deploy model** > **Deploy base model**
5. Select the model you want to deploy and select **Confirm**
6. Set the **Deployment name**, which is the name you use in API calls and the value AI Studio maps to your model
7. Select **Deploy**
8. Wait until the **Provisioning** state changes to **Succeeded**

For more details on deploying models on Azure OpenAI resources, see [Deploy a model](https://learn.microsoft.com/en-us/azure/foundry-classic/openai/how-to/create-resource?pivots=web-portal).

## Option 2: Foundry Models

### Create a Foundry project

Foundry Models are deployed within a Foundry project. Create one if you don't already have a project.

1. Sign in to [Microsoft Foundry](https://ai.azure.com). Ensure the **New Foundry** toggle is **on**
2. Select **+ New project** from the top navigation
3. Enter a **Project name**
4. Expand **Advanced options** to configure:

| Field                | Description                                                                                       |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| **Foundry resource** | The Foundry resource that manages this project                                                    |
| **Region**           | The Azure region for the project (for example, **East US 2**)                                     |
| **Subscription**     | The Azure subscription to bill against                                                            |
| **Resource group**   | The resource group to contain the project resources. Select an existing group or create a new one |

5. Select **Create**

For more details on creating Foundry projects, see [Create a Foundry project](https://learn.microsoft.com/en-us/azure/foundry/how-to/create-projects?tabs=foundry).

### Deploy a model

1. From the Foundry portal homepage, select **Discover** in the upper navigation, then **Models** in the left pane
2. Select a model and review its details
3. Select **Deploy** > **Custom settings** to configure the deployment (or **Default settings** for quick setup)
4. For partner and community models, read the terms of use and select **Agree and Proceed** to subscribe through Azure Marketplace
5. Set the **Deployment name**, which is used in API calls and the value AI Studio maps to your model
6. Select **Deploy**
7. Wait until the deployment status shows **Succeeded**

For more details, see [Deploy Foundry Models](https://learn.microsoft.com/en-us/azure/foundry/foundry-models/how-to/deploy-foundry-models) on Microsoft Learn.

## Retrieve the target URI and API key

After deploying a model through either setup path, navigate to the deployment to copy your credentials.

1. In the Foundry portal, navigate to the model you deployed
2. Copy the **Target URI** and **API Key** from the deployment details to [use in AI Studio](#add-microsoft-azure-models-in-ai-studio)

<Warning>
  Store your API key securely. If a key is compromised, rotate it from the
  deployment detail page or the Azure portal.
</Warning>

## Add Microsoft Azure models in AI Studio

After deploying a model and copying your endpoint and key from either setup path:

1. Navigate to **Models & Guardrails > Models** in [AI Studio](https://app.writer.com/aistudio)
2. Select **+ Add model**
3. Select **Microsoft Azure** as the provider
4. Select your model from the **Model** dropdown
5. Enter your credentials:
   * **API Base**: The Target URI from your deployment (for example, `https://your-resource.openai.azure.com/`)
   * **API Version**: The API version to use (for example, `2023-07-01-preview`)
   * **Azure API Key**: The API key from your deployment
6. Configure team access:
   * **All teams**: Anyone with builder access can use the model
   * **Specific teams**: Restrict to selected teams
7. Select **Save**

<img alt="" />

## Monitor costs

Azure bills usage directly to your Azure subscription based on tokens processed and deployment configuration. WRITER also charges a 10% pass-through fee on external model usage. AI Studio tracks usage and costs for external models, providing visibility into spending across all your models in one place.

For information about monitoring model health and automatic recovery, see [Monitor model health](/home/external-models#monitor-model-health).

## Troubleshoot Microsoft Azure configuration

### Invalid credentials error

If you see an "Invalid credentials" or "Authentication failed" error:

* Verify the API key is copied correctly without extra spaces
* Check that the key is still active in the Azure portal
* Ensure the key belongs to the same resource as the API Base URL

### Model not available error

If a model doesn't appear or returns an error:

* Confirm the model deployment shows **Succeeded** in the Azure portal or Foundry portal
* Verify the deployment name matches the model selected in AI Studio
* Check that the API version is supported for your deployment

### Connection failed error

If AI Studio cannot connect to your Azure resource:

* Confirm the API Base URL uses `https://` and matches the endpoint in your portal
* Verify the resource network settings allow public access
* Check [Azure service health](https://azure.status.microsoft/) for regional incidents

### Unhealthy model status

If a model shows as unhealthy in AI Studio:

* AI Studio automatically retries unhealthy models after a cooldown period
* For transient issues like temporary Azure outages, no action is needed
* For persistent issues, check the troubleshooting items above

## Next steps

* [Add external models](/home/external-models): Learn about managing external models in AI Studio
* [Choose a model](/home/models-overview): Compare Palmyra models with external provider models
* [Configure guardrails](/home/guardrails): Set up content safety and compliance policies for your AI agents
* [Deploy Foundry Models (Microsoft Learn)](https://learn.microsoft.com/en-us/azure/foundry/foundry-models/how-to/deploy-foundry-models): Full Foundry Models deployment walkthrough


# NVIDIA NIM
Source: https://dev.writer.com/providers/nvidia-nim

Add AI models from NVIDIA NIM to use in AI Studio agents

This guide shows you how to configure NVIDIA NIM as an external model provider in AI Studio. After setting up this provider, you can use foundation models from NVIDIA's API Catalog or your own self-hosted NIM deployments when building agents.

<Note>
  NVIDIA NIM supports both cloud-hosted models via the [NVIDIA API
  Catalog](https://build.nvidia.com/models) and self-hosted deployments via
  [NVIDIA Inference
  Microservices](https://docs.api.nvidia.com/nim/docs/introduction). See
  [Deployment options](#deployment-options) for details on each approach.
</Note>

## Prerequisites

Before adding NVIDIA NIM models to AI Studio, you need:

* An [NVIDIA Developer account](https://build.nvidia.com) with access to the API Catalog
* An NGC API key for authentication (see [Generate an NGC API key](#generate-an-ngc-api-key))
* For self-hosted deployments: A running NIM instance with a reachable endpoint (see [Configure self-hosted endpoints](#configure-self-hosted-endpoints))

<Warning>
  AI Studio supports **text generation** and **embedding** models from NVIDIA
  NIM. Video, audio, and image generation models are not supported.
</Warning>

## Generate an NGC API key

Generate an API key from the NGC console to authenticate with NVIDIA NIM.

1. Navigate to the [NGC API Key page](https://org.ngc.nvidia.com/setup/api-keys)
2. Select **Generate API Key**
3. Enter a description for your key (for example, `AI Studio integration`)
4. Under **Key Permissions**, ensure **NGC Catalog** access is included in the **Services Included** list
5. Select **Generate**
6. Copy the API key immediately

<Warning>
  The API key is only shown once. Copy it immediately and store it securely. If
  you lose the key, you must generate a new one.
</Warning>

## Deployment options

NVIDIA NIM offers two deployment approaches. Choose the one that matches your infrastructure requirements.

### Cloud-hosted (NVIDIA API Catalog)

Use NVIDIA's cloud-hosted models through the API Catalog for quick setup. This approach:

* Requires no infrastructure management
* Uses NVIDIA's hosted API endpoint (`https://integrate.api.nvidia.com/v1`)
* Provides access to NVIDIA-hosted and partner models available through the API Catalog
* Bills usage through your NVIDIA account

### Self-hosted (NVIDIA Inference Microservices)

Deploy NIM containers in your own infrastructure for greater control. This approach:

* Runs models on your own GPU or cloud instances
* Requires a custom API Base URL pointing to your NIM deployment
* Provides greater control over where inference runs and how infrastructure is managed
* Supports air-gapped or private network deployments

<Note>
  For self-hosted deployments, AI Studio must be able to send outbound HTTPS
  requests to your NIM endpoint. You may need to configure network rules or use
  a public endpoint.
</Note>

## Add NVIDIA NIM models in AI Studio

After generating your API key, add NVIDIA NIM models to AI Studio.

<Note>
  For self-hosted deployments, configure your NIM instance and note its API Base
  URL before proceeding. See [Configure self-hosted
  endpoints](#configure-self-hosted-endpoints) for details.
</Note>

1. Navigate to **Models & Guardrails > Models** in [AI Studio](https://app.writer.com/aistudio)
2. Select **+ Add model**
3. Select **NVIDIA NIM** as the provider
4. Enter your NGC API key
5. Configure the endpoint:
   * For cloud-hosted models: Leave the API Base URL empty to use the default NVIDIA endpoint
   * For self-hosted models: Enter your custom API Base URL (for example, `https://your-nim-instance.example.com/v1`)
6. Select the models you want to add from the available list
7. Configure team access:
   * **All teams**: Anyone with builder access can use the model
   * **Specific teams**: Restrict to selected teams
8. Select **Add model**

<img alt="" />

## Configure self-hosted endpoints

When using self-hosted NIM deployments, provide the base URL where your NIM instance is running. For detailed setup instructions, including hardware requirements and deployment options, see [Get started with NVIDIA NIM for LLMs](https://docs.nvidia.com/nim/large-language-models/latest/getting-started.html).

### API Base URL format

The API Base URL should point to your NIM deployment's API endpoint:

```
https://your-nim-host:port/v1
```

Replace `your-nim-host` and `port` with your actual NIM instance address.

### Verify your endpoint

Before adding to AI Studio, verify your NIM endpoint is accessible:

```bash theme={null}
curl https://your-nim-instance.example.com/v1/models \
  -H "Authorization: Bearer YOUR_API_KEY"
```

A successful response returns a list of available models on your NIM instance. For more information on available endpoints, see the [NVIDIA NIM API reference](https://docs.nvidia.com/nim/large-language-models/latest/api-reference.html).

## Monitor costs

For cloud-hosted models, NVIDIA bills usage directly to your NVIDIA account based on the tokens processed. WRITER also charges a 10% pass-through fee on external model usage. AI Studio tracks usage and costs for external models, providing visibility into spending across all your models in one place.

For self-hosted deployments, costs depend on your infrastructure (compute instances, resources).

For information about monitoring model health and automatic recovery, see [Monitor model health](/home/external-models#monitor-model-health).

## Troubleshoot NVIDIA NIM configuration

### Invalid API key error

If you see an "Invalid API key" or "Authentication failed" error:

* Verify the API key is copied correctly without extra spaces
* Check that the API key is still active in the NGC console
* Ensure you're using an NGC API key, not a different NVIDIA credential type

### Model not available error

If a model doesn't appear or returns an error:

* For cloud-hosted: Confirm the model is available in the NVIDIA API Catalog
* For self-hosted: Verify your NIM instance has the model deployed
* Check that your API key has access to the specific model

### Connection failed error

If AI Studio cannot connect to your NIM endpoint:

* Verify the API Base URL is correct and includes the protocol (`https://`)
* Check that your NIM instance is running and accessible
* Ensure network rules allow traffic from AI Studio's infrastructure
* For self-hosted deployments, verify SSL certificates are valid

### Unhealthy model status

If a model shows as unhealthy in AI Studio:

* AI Studio automatically retries unhealthy models after a cooldown period
* For cloud-hosted models experiencing transient issues, no action is needed
* For self-hosted models, check that your NIM instance is running and healthy
* For persistent issues, check the troubleshooting items above

## Next steps

* [Add external models](/home/external-models): Learn about managing external models in AI Studio
* [Choose a model](/home/models-overview): Compare Palmyra models with external provider models
* [NVIDIA NIM documentation](https://docs.nvidia.com/nim/): Explore NIM capabilities