GraphQL API#
Slices provides a powerful GraphQL API for querying indexed AT Protocol data. The API automatically generates schema from your lexicons and provides efficient querying with relationship traversal.
Accessing the API#
GraphQL endpoints are available per-slice:
POST /graphql?slice=<slice-uri>
GraphQL Playground#
Access the interactive GraphQL Playground in your browser:
https://api.slices.network/graphql?slice=<slice-uri>
Schema Generation#
The GraphQL schema is automatically generated from your slice's lexicons:
- Types: One GraphQL type per collection (e.g.,
social.grain.gallery→SocialGrainGallery) - Queries: Collection queries with filtering, sorting, and pagination
- Mutations: Create, update, delete operations per collection
- Subscriptions: Real-time updates for record changes
Querying Data#
Basic Query#
query {
socialGrainGalleries {
edges {
node {
uri
title
description
createdAt
}
}
}
}
Filtering#
Use where clauses with typed filter conditions. Each collection has its own
{Collection}WhereInput type with appropriate filters for each field.
query {
socialGrainGalleries(where: {
title: { contains: "Aerial" }
}) {
edges {
node {
uri
title
description
}
}
}
}
Filter Types#
The API provides three filter types based on field data types:
StringFilter - For string fields:
eq: Exact matchin: Match any value in arraycontains: Substring match (case-insensitive)fuzzy: Fuzzy/similarity match (typo-tolerant)gt: Greater than (lexicographic)gte: Greater than or equal tolt: Less thanlte: Less than or equal to
IntFilter - For integer fields:
eq: Exact matchin: Match any value in arraygt: Greater thangte: Greater than or equal tolt: Less thanlte: Less than or equal to
DateTimeFilter - For datetime fields:
eq: Exact matchgt: After datetimegte: At or after datetimelt: Before datetimelte: At or before datetime
Fuzzy Matching Example#
The fuzzy filter uses PostgreSQL's trigram similarity for typo-tolerant
search:
query FuzzySearch {
fmTealAlphaFeedPlays(
where: {
trackName: { fuzzy: "love" }
}
) {
edges {
node {
trackName
artists
}
}
}
}
This will match track names like:
- "Love" (exact)
- "Love Song"
- "Lovely"
- "I Love You"
- "Lover"
- "Loveless"
The fuzzy filter is great for:
- Handling typos and misspellings
- Finding similar variations of text
- Flexible search without exact matching
Note: Fuzzy matching works on the similarity between strings (using
trigrams), so it's more flexible than contains but may return unexpected
matches if the similarity threshold is met.
Date Range Example#
query RecentGalleries {
socialGrainGalleries(
where: {
createdAt: {
gte: "2025-01-01T00:00:00Z"
lt: "2025-12-31T23:59:59Z"
}
}
) {
edges {
node {
uri
title
createdAt
}
}
}
}
Multiple Conditions#
Combine multiple filters - they are AND'ed together:
query {
socialGrainGalleries(
where: {
title: { contains: "Aerial" }
createdAt: { gte: "2025-01-01T00:00:00Z" }
}
) {
edges {
node {
uri
title
description
createdAt
}
}
}
}
Nested AND/OR Queries#
Build complex filter logic with arbitrarily nestable and and or arrays:
Simple OR - Match any condition:
query {
networkSlicesSlices(
where: {
or: [
{ name: { contains: "grain" } }
{ name: { contains: "teal" } }
]
}
) {
edges {
node {
name
}
}
}
}
Simple AND - Match all conditions:
query {
networkSlicesSlices(
where: {
and: [
{ name: { contains: "grain" } }
{ name: { contains: "teal" } }
]
}
) {
edges {
node {
name
}
}
}
}
Complex Nested Logic:
query {
appBskyFeedPost(
where: {
and: [
{
or: [
{ text: { contains: "music" } }
{ text: { contains: "song" } }
]
}
{
and: [
{ uri: { contains: "app.bsky" } }
{ uri: { contains: "post" } }
]
}
{ createdAt: { gte: "2025-01-01T00:00:00Z" } }
]
}
) {
edges {
node {
uri
text
createdAt
}
}
}
}
This example finds posts where:
- (text contains "music" OR text contains "song") AND
- (uri contains "app.bsky" AND uri contains "post") AND
- createdAt is after 2025-01-01
Key Features:
- Unlimited nesting depth -
and/orcan be nested arbitrarily - Mix with field filters - combine nested logic with regular field conditions
- Type-safe - Each collection's
WhereInputsupportsandandorarrays - Available in queries and aggregations
Pagination#
Relay-style cursor pagination:
query {
socialGrainGalleries(first: 10, after: "cursor") {
edges {
cursor
node {
uri
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Sorting#
Each collection has its own typed {Collection}SortFieldInput for type-safe
sorting:
query {
socialGrainGalleries(
sortBy: [
{ field: createdAt, direction: desc }
]
) {
edges {
node {
uri
title
createdAt
}
}
}
}
Multi-field sorting:
query {
socialGrainGalleries(
sortBy: [
{ field: actorHandle, direction: asc }
{ field: createdAt, direction: desc }
]
) {
edges {
node {
uri
title
actorHandle
createdAt
}
}
}
}
The field enum values are collection-specific (e.g.,
SocialGrainGallerySortFieldInput). Use GraphQL introspection or the playground
to see available fields for each collection.
Aggregations#
Aggregation queries allow you to group records and perform calculations. Each
collection has a corresponding {Collection}Aggregated query.
Basic Aggregation#
Group records by one or more fields and get counts:
query TopTracks {
fmTealAlphaFeedPlaysAggregated(
groupBy: [{ field: trackName }]
orderBy: { count: desc }
limit: 10
) {
trackName
count
}
}
Multi-Field Grouping#
Group by multiple fields using the typed {Collection}GroupByField enum:
query TopTracksByArtist {
fmTealAlphaFeedPlaysAggregated(
groupBy: [{ field: trackName }, { field: artists }]
orderBy: { count: desc }
limit: 20
) {
trackName
artists
count
}
}
Filtering Aggregations#
Combine typed filters with aggregations for time-based analysis:
query TopTracksThisWeek {
fmTealAlphaFeedPlaysAggregated(
groupBy: [{ field: trackName }, { field: artists }]
where: {
indexedAt: {
gte: "2025-01-01T00:00:00Z"
lt: "2025-01-08T00:00:00Z"
}
trackName: { contains: "Love" }
}
orderBy: { count: desc }
limit: 10
) {
trackName
artists
count
}
}
Aggregation Features#
- Typed GroupBy: Each collection has a
{Collection}GroupByFieldenum for type-safe field selection - Typed Filters: Use the same
{Collection}WhereInputas regular queries - Sorting: Order by
count(ascending or descending) or any grouped field - Pagination: Use
limitto control result count - Multiple Fields: Group by any combination of fields from your lexicon
- Date Truncation: Group by time intervals (second, minute, hour, day, week, month, quarter, year)
Date Truncation#
Group records by time intervals using the interval parameter in groupBy:
query DailyPlays {
fmTealAlphaFeedPlaysAggregated(
groupBy: [
{ field: playedTime, interval: day }
]
orderBy: { count: desc }
limit: 30
) {
playedTime
count
}
}
Supported Intervals:
second- Group by secondminute- Group by minutehour- Group by hourday- Group by day (common for daily reports)week- Group by week (Monday-Sunday)month- Group by monthquarter- Group by quarter (Q1-Q4)year- Group by year
Combining with Regular Fields:
query TrackPlaysByDay {
fmTealAlphaFeedPlaysAggregated(
groupBy: [
{ field: trackName },
{ field: playedTime, interval: day }
]
orderBy: { count: desc }
limit: 100
) {
trackName
playedTime
count
}
}
How it Works:
- Uses PostgreSQL's
date_trunc()function for efficient time bucketing - Automatically handles timestamp casting for JSON fields
- Returns truncated timestamps (e.g.,
2025-01-15 00:00:00for day interval) - Works with both system fields (
indexedAt) and lexicon datetime fields
Use Cases#
Daily/Weekly/Monthly Reports:
query WeeklyPlays {
fmTealAlphaFeedPlaysAggregated(
groupBy: [{ field: trackName }]
where: {
playedTime: {
gte: "2025-01-01T00:00:00Z"
lt: "2025-01-08T00:00:00Z"
}
}
orderBy: { count: desc }
limit: 50
) {
trackName
count
}
}
Trend Analysis:
query TrendingArtists {
fmTealAlphaFeedPlaysAggregated(
groupBy: [{ field: artists }]
where: {
playedTime: { gte: "2025-01-01T00:00:00Z" }
}
orderBy: { count: desc }
limit: 20
) {
artists
count
}
}
Relationships#
The GraphQL API automatically generates relationship fields based on your
lexicon's at-uri fields.
Forward Joins (References)#
When a record has an at-uri field, you get a singular field that resolves
to the referenced record.
Lexicon Schema (social.grain.gallery.item):
{
"lexicon": 1,
"id": "social.grain.gallery.item",
"defs": {
"main": {
"type": "record",
"key": "tid",
"record": {
"type": "object",
"required": ["gallery", "item", "position", "createdAt"],
"properties": {
"gallery": {
"type": "string",
"format": "at-uri"
},
"item": {
"type": "string",
"format": "at-uri"
},
"position": { "type": "integer" },
"createdAt": { "type": "string", "format": "datetime" }
}
}
}
}
}
Generated GraphQL Type:
type SocialGrainGalleryItem {
uri: String!
gallery: String! # at-uri field from lexicon
item: String! # at-uri field from lexicon
position: Int!
createdAt: String!
# Auto-generated forward joins (singular)
socialGrainGallery: SocialGrainGallery
socialGrainPhoto: SocialGrainPhoto
}
Example Query:
query {
socialGrainGalleryItems(limit: 5) {
position
# Follow the reference to get the photo
socialGrainPhoto {
uri
alt
aspectRatio
}
# Follow the reference to get the gallery
socialGrainGallery {
title
description
}
}
}
Reverse Joins (Backlinks)#
When other records reference this record via at-uri fields, you get plural
fields that find all records pointing here.
Lexicon Schema (social.grain.favorite):
{
"lexicon": 1,
"id": "social.grain.favorite",
"defs": {
"main": {
"type": "record",
"key": "tid",
"record": {
"type": "object",
"required": ["subject", "createdAt"],
"properties": {
"subject": {
"type": "string",
"format": "at-uri"
},
"createdAt": { "type": "string", "format": "datetime" }
}
}
}
}
}
Generated GraphQL Types:
type SocialGrainFavorite {
uri: String!
subject: String! # at-uri pointing to gallery
createdAt: String!
# Forward join (follows the subject field)
socialGrainGallery: SocialGrainGallery
}
type SocialGrainGallery {
uri: String!
title: String
# Auto-generated reverse joins (plural)
# These find all records whose at-uri fields point here
socialGrainFavorites: [SocialGrainFavorite!]!
socialGrainComments: [SocialGrainComment!]!
socialGrainGalleryItems: [SocialGrainGalleryItem!]!
}
Example Query:
query {
socialGrainGalleries(where: {
actorHandle: { eq: "chadtmiller.com" }
}) {
edges {
node {
uri
title
# Get all favorites for this gallery
socialGrainFavorites {
uri
createdAt
actorHandle
}
# Get all comments for this gallery
socialGrainComments {
uri
text
actorHandle
}
}
}
}
}
Count Fields#
For efficient counting without loading all data, use *Count fields:
query {
socialGrainGalleries {
edges {
node {
uri
title
# Efficient count queries (no data loading)
socialGrainFavoritesCount
socialGrainCommentsCount
socialGrainPhotosCount
}
}
}
}
Combining Counts and Data#
Best practice: Get counts separately from limited data:
query {
socialGrainGalleries {
edges {
node {
uri
title
# Total count
socialGrainFavoritesCount
socialGrainCommentsCount
# Show preview (first 3)
socialGrainFavorites(limit: 3) {
uri
actorHandle
}
socialGrainComments(limit: 3) {
uri
text
}
}
}
}
}
DataLoader & Performance#
The GraphQL API uses DataLoader for efficient batching:
CollectionDidLoader#
- Batches queries by
(slice_uri, collection, did) - Used for forward joins where the DID is known
- Eliminates N+1 queries when following references
CollectionUriLoader#
- Batches queries by
(slice_uri, collection, parent_uri, reference_field) - Used for reverse joins based on at-uri fields
- Efficiently loads all records that reference a parent URI
- Supports multiple at-uri fields (tries each until match found)
Example: Loading 100 galleries with favorites
- Without DataLoader: 1 + 100 queries (N+1 problem)
- With DataLoader: 1 + 1 query (batched)
Complex Queries#
Nested Relationships#
query {
socialGrainGalleries {
edges {
node {
title
socialGrainGalleryItems {
position
socialGrainPhoto {
uri
alt
photo {
url(preset: "feed_fullsize")
}
socialGrainPhotoExifs {
fNumber
iSO
make
model
}
}
}
}
}
}
}
Full Example#
query MyGrainGalleries {
socialGrainGalleries(
where: { actorHandle: { eq: "chadtmiller.com" } }
sortBy: [{ field: createdAt, direction: desc }]
) {
edges {
node {
uri
title
description
createdAt
# Counts
socialGrainFavoritesCount
socialGrainCommentsCount
# Preview data
socialGrainFavorites(limit: 5) {
uri
createdAt
actorHandle
}
socialGrainComments(limit: 3) {
uri
text
createdAt
actorHandle
}
# Gallery items with nested photos
socialGrainGalleryItems {
position
socialGrainPhoto {
uri
alt
photo {
url(preset: "avatar")
}
aspectRatio
createdAt
socialGrainPhotoExifs {
fNumber
iSO
make
model
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Mutations#
Upload Blob#
Upload a blob (image, video, or other file) to your AT Protocol repository. The blob will be stored in your PDS and can be referenced in records.
mutation UploadBlob($data: String!, $mimeType: String!) {
uploadBlob(data: $data, mimeType: $mimeType) {
blob {
ref
mimeType
size
}
}
}
Parameters:
data(String, required): Base64-encoded file datamimeType(String, required): MIME type of the file (e.g., "image/jpeg", "image/png", "video/mp4")
Returns:
blob: A JSON blob object containing:ref: The CID (content identifier) reference for the blobmimeType: The MIME type of the uploaded blobsize: The size of the blob in bytes
Example with Variables:
{
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"mimeType": "image/png"
}
Usage in Records:
After uploading a blob, use the returned blob object in your record mutations:
mutation UpdateProfile($avatar: JSON) {
updateAppBskyActorProfile(
rkey: "self"
input: {
displayName: "My Name"
avatar: $avatar # Use the blob object from uploadBlob
}
) {
uri
displayName
avatar {
ref
mimeType
size
url(preset: "avatar")
}
}
}
Create Records#
Create new records in your AT Protocol repository. Each collection has a typed create{Collection} mutation with a corresponding {Collection}Input type.
mutation CreateFollow {
createAppBskyGraphFollow(
input: {
subject: "did:plc:z72i7hdynmk6r22z27h6tvur"
createdAt: "2025-01-15T12:00:00Z"
}
) {
uri
cid
subject
createdAt
}
}
Parameters:
input(required): Typed input object with the fields defined in your lexiconrkey(optional): Record key for the new record. If not provided, a TID (timestamp identifier) is automatically generated
Returns: The complete created record with all fields, including generated fields like uri, cid, did, and indexedAt.
Example with custom rkey:
mutation CreateFollowWithRkey {
createAppBskyGraphFollow(
input: {
subject: "did:plc:z72i7hdynmk6r22z27h6tvur"
createdAt: "2025-01-15T12:00:00Z"
}
rkey: "my-custom-key"
) {
uri
subject
createdAt
}
}
Update Records#
Update existing records by their rkey. Each collection has an update{Collection} mutation.
Important: Updates replace the entire record. You must provide all required fields, not just the fields you want to change.
mutation UpdateProfile {
updateAppBskyActorProfile(
rkey: "self"
input: {
displayName: "New Display Name"
description: "Updated bio"
avatar: $avatarBlob
banner: $bannerBlob
}
) {
uri
displayName
description
avatar {
url(preset: "avatar")
}
}
}
Parameters:
rkey(String, required): The record key of the record to updateinput(required): Complete record data (all required fields must be provided)
Returns: The complete updated record with all fields.
Notes:
- Updates perform a full record replacement via AT Protocol's
putRecord - All required fields from your lexicon must be included in
input - To partially update, first fetch the existing record, merge your changes, then update with complete data
- The rkey is the last segment of the record's URI (e.g.,
at://did:plc:abc/app.bsky.actor.profile/self→ rkey isself)
Delete Records#
Delete records by their rkey. Each collection has a delete{Collection} mutation.
mutation DeleteFollow {
deleteAppBskyGraphFollow(rkey: "3kjvbfz5nw42a") {
uri
subject
}
}
Parameters:
rkey(String, required): The record key of the record to delete
Returns: The deleted record with its data (before deletion).
Example - Delete a profile:
mutation DeleteProfile {
deleteAppBskyActorProfile(rkey: "self") {
uri
displayName
}
}
Naming Convention#
All mutations follow a consistent naming pattern based on the lexicon collection name:
| Collection | Create | Update | Delete |
|---|---|---|---|
app.bsky.actor.profile |
createAppBskyActorProfile |
updateAppBskyActorProfile |
deleteAppBskyActorProfile |
app.bsky.graph.follow |
createAppBskyGraphFollow |
updateAppBskyGraphFollow |
deleteAppBskyGraphFollow |
social.grain.gallery |
createSocialGrainGallery |
updateSocialGrainGallery |
deleteSocialGrainGallery |
social.grain.photo |
createSocialGrainPhoto |
updateSocialGrainPhoto |
deleteSocialGrainPhoto |
The pattern is: {action}{PascalCaseCollection} where dots in the collection name are removed and each segment is capitalized.
Subscriptions#
Real-time updates for record changes. Each collection has three subscription fields:
Created Records#
Subscribe to newly created records:
subscription {
socialGrainGalleryCreated {
uri
title
description
createdAt
}
}
Updated Records#
Subscribe to record updates:
subscription {
socialGrainGalleryUpdated {
uri
title
description
updatedAt
}
}
Deleted Records#
Subscribe to record deletions (returns just the URI):
subscription {
socialGrainGalleryDeleted
}
Limits & Performance#
- Depth Limit: 50 (supports introspection with circular relationships)
- Complexity Limit: 5000 (prevents expensive queries)
- Default Limit: 50 records per query
- DataLoader: Automatic batching eliminates N+1 queries
Best Practices#
- Use count fields when you only need totals
- Limit nested data with
limitparameter - Request only needed fields (no over-fetching)
- Use cursors for pagination, not offset
- Batch related queries with DataLoader (automatic)
- Combine counts + limited data for previews
Error Handling#
GraphQL errors include:
"Query is nested too deep"- Exceeds depth limit (50)"Query is too complex"- Exceeds complexity limit (5000)"Schema error"- Invalid slice or missing lexicons