# AI-Powered Search

Discover assets using natural language and semantic understanding with Playbook's AI Search.

## Overview[​](#overview "Direct link to Overview")

AI Search goes beyond keyword matching to understand the meaning and context of your queries. Instead of exact text matches, AI Search finds assets based on visual similarity, concept understanding, and semantic relevance.

### AI Search vs. Regular Search[​](#ai-search-vs-regular-search "Direct link to AI Search vs. Regular Search")

| Feature                  | Regular Search        | AI Search              |
| ------------------------ | --------------------- | ---------------------- |
| **Method**               | Text/keyword matching | Semantic understanding |
| **Query Style**          | Exact terms           | Natural language       |
| **Results**              | Title/tag matches     | Concept-based matches  |
| **Visual Understanding** | No                    | Yes                    |
| **Best For**             | Known filenames       | Exploring, discovering |

***

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

* **Access Token**: API token with search permissions
* **Organization Slug**: Your organization identifier
* **AI Search Access**: Feature must be enabled for your organization

***

## Basic AI Search[​](#basic-ai-search "Direct link to Basic AI Search")

### Simple Query[​](#simple-query "Direct link to Simple Query")

```
curl "https://api.playbook.com/v1/my-org/ai_search?query=sunset+beach+photos" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response:**

```
{

  "data": [

    {

      "id": 301,

      "token": "coastal-sunset",

      "title": "Golden Hour Coast",

      "media_type": "image/jpeg",

      "display_url": "https://cdn.playbook.com/coastal-sunset.jpg"

    },

    {

      "id": 302,

      "token": "beach-evening",

      "title": "Evening Beach Walk",

      "media_type": "image/jpeg",

      "display_url": "https://cdn.playbook.com/beach-evening.jpg"

    }

  ],

  "pagy": {

    "parent_id": "search-token-123",

    "after_cursor": "0.85@2026-08-21T12:00:00Z@1",

    "has_next_page": true

  }

}
```

### Natural Language Queries[​](#natural-language-queries "Direct link to Natural Language Queries")

AI Search understands natural language - ask questions the way you'd ask a person:

```
# Find conceptually related assets

"images that would work well for a summer campaign"



# Describe what you're looking for

"product photos with white background and good lighting"



# Ask for specific moods or styles

"minimalist designs with lots of negative space"



# Find by use case

"images suitable for social media headers"
```

***

## Advanced Queries[​](#advanced-queries "Direct link to Advanced Queries")

### Combining Concepts[​](#combining-concepts "Direct link to Combining Concepts")

```
# Multiple concepts

curl "https://api.playbook.com/v1/my-org/ai_search?query=professional+headshot+bright+background+smiling" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

### Filtering AI Search Results[​](#filtering-ai-search-results "Direct link to Filtering AI Search Results")

Combine AI search with traditional filters for precision:

```
curl --globoff "https://api.playbook.com/v1/my-org/ai_search?query=modern+workspace&filters[media_type]=image/jpeg&filters[boards][]=office-photos" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

**Available Filters.** Note that `boards` and `recursive_boards` are alternatives — sending both in one request returns `422`, so only `boards` appears here. See *Scoping to a board* just below for which one to use.

```
{

  "filters": {

    "media_type": "image/jpeg",

    "boards": ["board-token-1", "board-token-2"],

    "title": "contains-this-text",

    "tags": ["approved", "branding"],

    "tags_op": "and",

    "statuses": ["Approved"],

    "uploaded_by": ["asg_9f2c8a1b"],

    "date_from": "2026-08-13",

    "date_to": "2026-08-20",

    "date_field": "uploaded"

  }

}
```

`statuses` matches status and custom field **values** — the built-in Status and every custom field alike — case-insensitively, and several values match any of them. Unlike the keyword [search endpoint](/docs/guides/search.md) there is no `statuses_op`: AI Search has no `and` mode for statuses.

### Scoping to a board — and why `boards` alone may return nothing[​](#scoping-to-a-board--and-why-boards-alone-may-return-nothing "Direct link to scoping-to-a-board--and-why-boards-alone-may-return-nothing")

**With no board filter, AI Search covers the entire workspace** — every board you can see. For a moodboard, a pitch reel or a client hand-off that usually means most of what comes back belongs to other projects, so scope the search.

`boards` matches an asset's **own** board. A board whose assets all live in sub-boards therefore matches nothing, and the search comes back empty as though the board had nothing like your query in it. Use `recursive_boards` when the token might be a parent: it matches the board **and everything nested under it**.

```
curl --globoff "https://api.playbook.com/v1/my-org/ai_search?query=warm+interior+light&filters[recursive_boards][]=client-delivery" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

The two are mutually exclusive — sending both returns `422`, because only one board clause can be applied and picking one for you would silently answer a different question.

note

Subtree matching is served by the search index, which is refreshed when an *asset* is re-indexed, so it can briefly lag a *board* being moved. When you need a guaranteed-complete list of everything under a board rather than a relevance-ranked one, use `GET /v1/{slug}/assets?collection_token=…&nested_assets=true` — it reads the database directly. See [Fetching Data](/docs/guides/fetching_data.md).

### Filtering by who uploaded, and when[​](#filtering-by-who-uploaded-and-when "Direct link to Filtering by who uploaded, and when")

`uploaded_by` takes workspace member tokens — look a person up with `GET /v1/{slug}/users?query=` first. A plain name is accepted as a fallback and resolved for you, but only when it matches exactly one member; anything ambiguous fails with `422` and lists the candidates rather than guessing.

`date_field` chooses which date `date_from`/`date_to` apply to: `original` (the file's own creation date, from EXIF where available) or `uploaded` (when it was added to Playbook). It defaults to `original` — **except when `uploaded_by` is also present, where it defaults to `uploaded`**, so "what did this person upload last week" means what you would expect.

```
curl --globoff "https://api.playbook.com/v1/my-org/ai_search?query=&filters[uploaded_by][]=asg_9f2c8a1b&filters[date_from]=2026-08-13" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

note

A person's name in `query` searches what is *depicted* in an asset, not who added it. To filter by a person, always resolve them to a token first — see [People and Uploaders](/docs/guides/people.md).

***

## Pagination[​](#pagination "Direct link to Pagination")

AI Search uses cursor-based pagination for optimal performance.

### First Page[​](#first-page "Direct link to First Page")

```
curl "https://api.playbook.com/v1/my-org/ai_search?query=product+photography" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

### Next Page[​](#next-page "Direct link to Next Page")

Use both `parent_id` and `after_cursor` from the previous response. The cursor is scoped to that saved search; sending it without its parent starts a new first-page query instead of continuing the existing one.

```
curl "https://api.playbook.com/v1/my-org/ai_search?query=product+photography&parent_id=search-token-123&after_cursor=0.85%402026-08-21T12%3A00%3A00Z%401" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

### JavaScript Example[​](#javascript-example "Direct link to JavaScript Example")

```
async function searchAllPages(query) {

  const allResults = [];

  let cursor = null;

  let parentId = null;



  while (true) {

    const params = new URLSearchParams({

      query: query

    });



    if (cursor) {

      params.append('parent_id', parentId);

      params.append('after_cursor', cursor);

    }



    const response = await fetch(

      `https://api.playbook.com/v1/my-org/ai_search?${params}`,

      { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

    );



    const { data, pagy } = await response.json();

    allResults.push(...data);



    if (!pagy.has_next_page) break;

    parentId = pagy.parent_id;

    cursor = pagy.after_cursor;

  }



  return allResults;

}
```

***

## Refinement with Parent Queries[​](#refinement-with-parent-queries "Direct link to Refinement with Parent Queries")

Use parent queries to refine and narrow down search results:

### Initial Search[​](#initial-search "Direct link to Initial Search")

```
curl "https://api.playbook.com/v1/my-org/ai_search?query=office+interior" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

Use the response's `pagy.parent_id` value as `parent_id`.

### Refined Search[​](#refined-search "Direct link to Refined Search")

```
curl "https://api.playbook.com/v1/my-org/ai_search?query=modern+minimalist&parent_id=search-token-123" \

  -H "Authorization: Bearer YOUR_TOKEN"
```

This narrows down the "office interior" results to only modern minimalist styles.

***

## Use Cases[​](#use-cases "Direct link to Use Cases")

### 1. Visual Discovery[​](#1-visual-discovery "Direct link to 1. Visual Discovery")

Find assets based on visual characteristics:

```
# By color palette

"images with warm autumn colors"



# By composition

"photos with central subject and blurred background"



# By style

"illustrations with flat design aesthetic"
```

### 2. Conceptual Search[​](#2-conceptual-search "Direct link to 2. Conceptual Search")

Search by abstract concepts:

```
"assets that convey trust and professionalism"

"images that feel energetic and dynamic"

"designs with a vintage feel"
```

### 3. Smart Asset Discovery[​](#3-smart-asset-discovery "Direct link to 3. Smart Asset Discovery")

Help users find assets they didn't know how to describe:

```
// User interface example

async function smartAssetFinder(userDescription) {

  const response = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/ai_search`,

    {

      method: 'GET',

      headers: {

        'Authorization': `Bearer ${ACCESS_TOKEN}`

      },

      params: {

        query: userDescription,

        filters: {

          media_type: 'image/*',

          tags: ['approved']

        }

      }

    }

  );



  return await response.json();

}



// Usage

const results = await smartAssetFinder(

  "bright cheerful images for kids product page"

);
```

### 4. Content Recommendations[​](#4-content-recommendations "Direct link to 4. Content Recommendations")

Build "similar assets" features:

```
async function findSimilarAssets(assetToken) {

  // First, get the asset details

  const asset = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}`,

    { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

  ).then(r => r.json());



  // Use AI search with asset characteristics

  const query = `${asset.data.title} ${asset.data.tags.join(' ')}`;



  const similar = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/ai_search?query=${encodeURIComponent(query)}`,

    { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

  ).then(r => r.json());



  // Filter out the original asset

  return similar.data.filter(a => a.token !== assetToken);

}
```

***

## Best Practices[​](#best-practices "Direct link to Best Practices")

### 1. Query Construction[​](#1-query-construction "Direct link to 1. Query Construction")

**Good Queries:**

```
✅ "professional product photos with white background"

✅ "landscape images with mountains and blue sky"

✅ "minimalist logo designs in monochrome"
```

**Less Effective:**

```
❌ "good images" (too vague)

❌ "IMG_1234.jpg" (use regular search for filenames)

❌ "red" (too generic, be more specific)
```

### 2. Combining Search Types[​](#2-combining-search-types "Direct link to 2. Combining Search Types")

Use AI Search for discovery, then refine with regular search:

```
async function hybridSearch(concept, exactTags) {

  // Start with AI search for concept

  const aiResults = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/ai_search?query=${concept}`,

    { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

  ).then(r => r.json());



  // Filter by exact tags

  const filtered = aiResults.data.filter(asset =>

    exactTags.every(tag => asset.tags.includes(tag))

  );



  return filtered;

}



// Usage

const results = await hybridSearch(

  "corporate headshots with natural lighting",

  ["approved", "2024"]

);
```

### 3. Performance Optimization[​](#3-performance-optimization "Direct link to 3. Performance Optimization")

**Cache Common Searches:**

```
const searchCache = new Map();



async function cachedAISearch(query, ttl = 3600000) {

  const cacheKey = `ai_search:${query}`;



  if (searchCache.has(cacheKey)) {

    const cached = searchCache.get(cacheKey);

    if (Date.now() - cached.timestamp < ttl) {

      return cached.data;

    }

  }



  const response = await fetch(

    `https://api.playbook.com/v1/${ORG_SLUG}/ai_search?query=${encodeURIComponent(query)}`,

    { headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }

  );



  const data = await response.json();



  searchCache.set(cacheKey, {

    data,

    timestamp: Date.now()

  });



  return data;

}
```

### 4. User Experience Tips[​](#4-user-experience-tips "Direct link to 4. User Experience Tips")

**Provide Search Suggestions:**

```
const searchSuggestions = [

  "Product photos with clean backgrounds",

  "Team photos in office settings",

  "Lifestyle images with natural lighting",

  "Brand assets in primary colors"

];



function showSearchHints() {

  return searchSuggestions.map(suggestion => ({

    label: suggestion,

    onClick: () => performAISearch(suggestion)

  }));

}
```

***

## Complete Implementation Example[​](#complete-implementation-example "Direct link to Complete Implementation Example")

Here's a full search interface implementation:

```
class PlaybookAISearch {

  constructor(orgSlug, accessToken) {

    this.orgSlug = orgSlug;

    this.accessToken = accessToken;

    this.baseUrl = 'https://api.playbook.com/v1';

  }



  async search(query, options = {}) {

    const params = new URLSearchParams({

      query: query

    });



    // Add filters if provided

    if (options.filters) {

      if (options.filters.media_type) {

        params.append('filters[media_type]', options.filters.media_type);

      }

      if (options.filters.boards) {

        options.filters.boards.forEach(board => {

          params.append('filters[boards][]', board);

        });

      }

      if (options.filters.tags) {

        options.filters.tags.forEach(tag => {

          params.append('filters[tags][]', tag);

        });

      }

    }



    // A parent alone refines a search; a cursor must always travel with its parent.

    if (options.after_cursor && !options.parent_id) {

      throw new Error('after_cursor requires parent_id from the same response');

    }

    if (options.parent_id) {

      params.append('parent_id', options.parent_id);

    }

    if (options.after_cursor) {

      params.append('after_cursor', options.after_cursor);

    }



    const response = await fetch(

      `${this.baseUrl}/${this.orgSlug}/ai_search?${params}`,

      { headers: { Authorization: `Bearer ${this.accessToken}` } }

    );



    if (!response.ok) {

      throw new Error(`AI Search failed: ${response.statusText}`);

    }



    return await response.json();

  }



  async *searchStream(query, options = {}) {

    let cursor = options.after_cursor ?? null;

    let parentId = options.parent_id ?? null;



    while (true) {

      const results = await this.search(query, {

        ...options,

        ...(cursor ? { after_cursor: cursor, parent_id: parentId } : {}),

      });



      yield results.data;



      if (!results.pagy.has_next_page) break;

      parentId = results.pagy.parent_id;

      cursor = results.pagy.after_cursor;

    }

  }



  async searchAll(query, options = {}) {

    const allResults = [];



    for await (const batch of this.searchStream(query, options)) {

      allResults.push(...batch);

    }



    return allResults;

  }

}



// Usage

const search = new PlaybookAISearch('my-org', 'your_token');



// Simple search

const results = await search.search("sunset beach photos");



// Filtered search

const filtered = await search.search("product photography", {

  filters: {

    media_type: "image/jpeg",

    tags: ["approved", "2024"]

  }

});



// Get all results across pages

const allResults = await search.searchAll("office interiors");



// Stream results for progressive loading

for await (const batch of search.searchStream("brand assets")) {

  console.log(`Loaded ${batch.length} assets`);

  displayAssets(batch);

}
```

***

## Error Handling[​](#error-handling "Direct link to Error Handling")

### Common Errors[​](#common-errors "Direct link to Common Errors")

**401: Unauthenticated**

```
{

  "error": "Unauthenticated"

}
```

Solution: Check your access token is valid.

**403: AI Search not available**

```
{

  "error": "AI Search is not available for your organization"

}
```

Solution: Contact support to enable AI Search for your plan.

**400: Invalid query**

```
{

  "error": "Query parameter is required"

}
```

Solution: Ensure the query parameter is not empty.

***

## Comparison: When to Use Each Search Type[​](#comparison-when-to-use-each-search-type "Direct link to Comparison: When to Use Each Search Type")

### Use Regular Search When:[​](#use-regular-search-when "Direct link to Use Regular Search When:")

* Searching for specific filenames
* Looking up assets by exact tags
* Filtering by board membership
* You know the exact terms used in titles

### Use AI Search When:[​](#use-ai-search-when "Direct link to Use AI Search When:")

* Exploring your asset library
* Searching by visual concepts
* Finding assets for specific use cases
* Looking for aesthetically similar assets
* Users describe what they want in natural language

### Use Both Together:[​](#use-both-together "Direct link to Use Both Together:")

```
async function comprehensiveSearch(query, tags) {

  // Get AI search results

  const aiResults = await aiSearch(query);



  // Filter by exact tags

  const filtered = aiResults.filter(asset =>

    tags.every(tag => asset.tags.includes(tag))

  );



  return filtered;

}
```

***

## Related API Endpoints[​](#related-api-endpoints "Direct link to Related API Endpoints")

* [AI Search](/docs/api/ai-search-organization-assets.md)
* [Regular Search](/docs/api/search-organization-assets.md)
* [List Assets](/docs/api/organization-assets.md)

***

## Next Steps[​](#next-steps "Direct link to Next Steps")

* Learn about [Asset Management](/docs/guides/asset_management.md) to organize search results
* Explore [Custom Fields](/docs/guides/custom_fields.md) for additional filtering
* Read about [Webhooks](/docs/guides/webhooks.md) to automate workflows based on search results
