Skip to main content

AI-Powered Search

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

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.

FeatureRegular SearchAI Search
MethodText/keyword matchingSemantic understanding
Query StyleExact termsNatural language
ResultsTitle/tag matchesConcept-based matches
Visual UnderstandingNoYes
Best ForKnown filenamesExploring, discovering

Prerequisites

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

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

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

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

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 there is no statuses_op: AI Search has no and mode for statuses.

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.

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 originalexcept 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.


Pagination

AI Search uses cursor-based pagination for optimal performance.

First Page

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

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

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

Use parent queries to refine and narrow down search results:

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.

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

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"

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

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

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

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

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

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

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

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

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

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:

  • 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:

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;
}


Next Steps