All posts
entity-queriessirispotlightapp-entitytutorial

Entity queries: how Axint generates AppEntity for Siri and Spotlight search

defineEntity() generates AppEntity structs and EntityQuery conformances. Make your app's data searchable through Siri and Spotlight.

Axint TeamSaturday, April 11, 20268 min read

Siri can take parameters through App Intents. But without AppEntity, it can't search your app's data — users have to type exact values instead of picking from a list. Entity queries fix this. They're also the most boilerplate-heavy part of App Intents.

A single entity requires an AppEntity struct with displayRepresentation, a typeDisplayRepresentation, an EntityQuery with entities(for:), suggestedEntities(), and optionally allEntities() for Spotlight indexing. About 30 lines of mechanical code per entity. Twenty entities = 600 lines.

Axint generates all of it from a TypeScript definition.

typescript
const noteEntity = defineEntity({
  name: "Note",
  typeDisplayName: "Note",
  fields: {
    id: "UUID",
    title: "String",
    content: "String",
  },
  displayRepresentation: {
    stringLiteral: "title",
  },
  query: {
    suggestedEntities: { returns: "Note[]" },
    entitiesByIdentifier: {
      params: { identifiers: "UUID[]" },
      returns: "Note[]",
    },
    searchableEntities: {
      params: { searchTerm: "String" },
      returns: "Note[]",
    },
  },
});

Compiles to a complete AppEntity + EntityQuery:

swift
struct Note: AppEntity {
  var id: UUID
  var title: String
  var content: String

  static let defaultQuery = NoteQuery()

  var displayRepresentation: DisplayRepresentation {
    DisplayRepresentation(stringLiteral: title)
  }

  static var typeDisplayRepresentation: TypeDisplayRepresentation {
    TypeDisplayRepresentation(name: "Note")
  }
}

struct NoteQuery: EntityQuery {
  func entities(for identifiers: [UUID]) async throws -> [Note] {
    fatalError("Implement entities(for:)")
  }

  func suggestedEntities() async throws -> [Note] {
    fatalError("Implement suggestedEntities()")
  }
}

The fatalError() stubs force you to implement the data-fetching logic. Axint handles the structure; you provide the database calls.

Using entities in intents

Once you have an entity, reference it in an intent:

typescript
const searchNotes = defineIntent({
  name: "SearchNotes",
  title: "Search Notes",
  params: {
    note: param.entity(noteEntity, "Note to search"),
  },
});

Siri will now show a list of notes (from suggestedEntities()) when the user triggers SearchNotes. No manual entity wiring.

Property-based filtering

For filtered queries, Axint generates PropertyBasedEntityQuery conformances:

typescript
const noteEntity = defineEntity({
  name: "Note",
  fields: { id: "UUID", title: "String", tags: "String[]" },
  query: {
    entitiesByProperty: {
      property: "tags",
      params: { tag: "String" },
      returns: "Note[]",
    },
  },
});

Generates:

swift
extension NoteQuery: PropertyBasedEntityQuery {
  nonisolated static var properties = QueryProperties {
    Property(\[Note.self\], where: \.tags, matches: \.tag) { notes, tag in
      notes.filter { $0.tags.contains(tag) }
    }
  }
}

Siri can now ask: "Search notes tagged 'urgent'."

Spotlight indexing

Implement allEntities() and Spotlight will periodically index your data:

swift
func allEntities() async throws -> [Note] {
  return try await database.fetchAllNotes()
}

Users find your content from the home screen. The SPM plugin compiles entity definitions alongside your intent files, so everything stays in sync.

Current limitations

v0.3.8 doesn't support sorting criteria, compound predicates, or pagination hints. These are coming in v0.4. For now, implement complex filtering in your Swift stubs.