# Context Engineering

⏱️ **Time to Complete:** 15 minutes\
🎯 **Level:** Intermediate\
💻 **Language:** TypeScript

## What You'll Learn

2025 patterns for semantic memory:

* ✅ Form memory intentionally (summaries, entities, embeddings)
* ✅ Retrieve the right slice of context every time
* ✅ Align conversations to domains, tenants, and entities
* ✅ Control context windows and reranking without hand-editing prompts
* ✅ Ship production-safe retrieval with the same configuration the Graphlit SDK uses in Zine

[**📁 Reference implementation**](https://github.com/graphlit/graphlit-samples/tree/main/nextjs/chat-graph)

***

## Prerequisites

* Complete the [Quickstart tutorial](/getting-started/quickstart.md)
* Graphlit credentials configured in `.env` (from Getting Started guide)
* `npm install graphlit-client dotenv`

{% hint style="info" %}
Need Python or .NET samples? Open **Ask Graphlit** inside the Developer Portal (or visit [ask.graphlit.dev](https://ask.graphlit.dev)) and it will translate every call shown here into your SDK of choice.
{% endhint %}

***

## Shared Setup

```typescript
import { Graphlit } from 'graphlit-client';
import {
  ConversationSearchTypes,
  ContentTypes,
  EntityExtractionServiceTypes,
  FilePreparationServiceTypes,
  ModelServiceTypes,
  ObservableTypes,
  OpenAiModels,
  RetrievalStrategyTypes,
  RerankingModelServiceTypes,
  SearchTypes,
  SpecificationTypes,
  SummarizationTypes,
} from 'graphlit-client/dist/generated/graphql-types';

export const graphlit = new Graphlit();
```

***

## Pattern 1 – Form the Right Memory Up Front

Create a workflow that produces summaries, extracts entities, and respects your chunk budget before anything hits retrieval.

```typescript
export async function createContextWorkflow() {
  const summarySpec = await graphlit.createSpecification({
    name: 'Context Summaries',
    type: SpecificationTypes.Summarization,
    serviceType: ModelServiceTypes.OpenAi,
    openAI: {
      model: OpenAiModels.Gpt5Mini_400K,
      temperature: 0,
    },
  });

  const entitiesSpec = await graphlit.createSpecification({
    name: 'Context Entities',
    type: SpecificationTypes.Extraction,
    serviceType: ModelServiceTypes.OpenAi,
    openAI: {
      model: OpenAiModels.Gpt5_400K,
      temperature: 0,
    },
  });

  const workflow = await graphlit.createWorkflow({
    name: 'Context Engineering Workflow',
    preparation: {
      jobs: [
        {
          connector: {
            type: FilePreparationServiceTypes.Document,
            document: { includeImages: true },
          },
        },
      ],
      summarizations: [
        {
          type: SummarizationTypes.Summary,
          tokens: 400,
          specification: { id: summarySpec.createSpecification.id },
        },
      ],
    },
    extraction: {
      jobs: [
        {
          connector: {
            type: EntityExtractionServiceTypes.ModelText,
            modelText: {
              specification: { id: entitiesSpec.createSpecification.id },
              tokenThreshold: 48,
            },
            extractedTypes: [
              ObservableTypes.Person,
              ObservableTypes.Organization,
              ObservableTypes.Product,
              ObservableTypes.Event,
            ],
          },
        },
      ],
    },
  });

  console.log('Workflow ready:', workflow.createWorkflow.id);
  return workflow.createWorkflow.id;
}
```

**Run once:**

```bash
npx tsx scripts/create-context-workflow.ts
```

### Ingest with the Workflow

```typescript
export async function ingestNotebook(workflowId: string) {
  const response = await graphlit.ingestText(
    `Acme Corp escalation call with Sarah Chen (CTO) and Mike Rodriguez (VP Engineering).
    Action items:
    - Provide dedicated OAuth sandbox credentials.
    - Ship rate-limit dashboard before Q4 launch.
    Follow-up demo booked for next Tuesday.`,
    'Acme escalation call',
    undefined,
    undefined,
    undefined,
    undefined,
    true,
    { id: workflowId },
  );

  console.log('Content ingested:', response.ingestText.id);
  return response.ingestText.id;
}
```

Resulting content now ships with:

* Summaries referenced by `content.summary`
* Extracted observations for people, orgs, products, events
* Images preserved for downstream vision models

***

## Pattern 2 – Retrieval That Matches the Question

### Hybrid vs Keyword vs Vector

```typescript
export async function demoSearchModes(prompt: string) {
  const hybrid = await graphlit.queryContents({
    search: prompt,
    searchType: SearchTypes.Hybrid,
    limit: 10,
  });

  const keyword = await graphlit.queryContents({
    search: prompt,
    searchType: SearchTypes.Keyword,
    limit: 10,
  });

  const vector = await graphlit.queryContents({
    search: prompt,
    searchType: SearchTypes.Vector,
    limit: 10,
  });

  console.log({
    hybridMatches: hybrid.contents?.results?.length ?? 0,
    keywordMatches: keyword.contents?.results?.length ?? 0,
    vectorMatches: vector.contents?.results?.length ?? 0,
  });
}
```

Use **Hybrid** by default, fall back to **Keyword** when you need exact IDs or error codes, and **Vector** for conceptual prompts.

### Filter to the Right Domain and Entity

```typescript
export async function querySupportIncidents() {
  // First, find your collection by name (realistic production pattern)
  const collections = await graphlit.queryCollections({
    filter: { name: 'Support Documents' }
  });
  const supportCollectionId = collections.collections?.results?.[0]?.id;

  // Find your feed by name
  const feeds = await graphlit.queryFeeds({
    filter: { name: 'Support Slack Channel' }
  });
  const supportFeedId = feeds.feeds?.results?.[0]?.id;

  // Find specific entity (e.g., organization "Acme Corp")
  const observables = await graphlit.queryObservables({
    filter: { 
      types: [ObservableTypes.Organization],
      name: 'Acme Corp'
    }
  });
  const acmeOrgId = observables.observables?.results?.[0]?.id;

  // Now filter content using those IDs
  const thirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toISOString();
  const today = new Date().toISOString();

  const results = await graphlit.queryContents({
    search: 'authentication timeout',
    searchType: SearchTypes.Hybrid,
    limit: 12,
    types: [ContentTypes.Message, ContentTypes.Issue],
    creationDateRange: { from: thirtyDaysAgo, to: today },
    feeds: supportFeedId ? [{ id: supportFeedId }] : undefined,
    collections: supportCollectionId ? [{ id: supportCollectionId }] : undefined,
    observations: acmeOrgId
      ? [
          {
            type: ObservableTypes.Organization,
            observable: { id: acmeOrgId },
          },
        ]
      : undefined,
  });

  return results.contents?.results ?? [];
}
```

Filtering on collections, feeds, or observations keeps retrieval scoped to the exact tenant, product line, or account team that matters to the current user.

***

## Pattern 3 – Specifications That Enforce Context Rules

Build a conversation specification once, reuse it everywhere.

```typescript
export async function createSupportSpec() {
  const specification = await graphlit.createSpecification({
    name: 'Support Context (Section Retrieval)',
    type: SpecificationTypes.Completion,
    serviceType: ModelServiceTypes.OpenAi,
    openAI: {
      model: OpenAiModels.Gpt5_400K,
      temperature: 0.15,
      chunkTokenLimit: 480,
    },
    searchType: ConversationSearchTypes.Hybrid,
    retrievalStrategy: {
      type: RetrievalStrategyTypes.Section,
      contentLimit: 8,
    },
    rerankingStrategy: {
      serviceType: RerankingModelServiceTypes.Cohere,
      threshold: 0.35,
    },
  });

  console.log('Specification ready:', specification.createSpecification.id);
  return specification.createSpecification.id;
}
```

* **Section retrieval** keeps context slices coherent (page/segment level).
* **Reranking** uses Cohere to score relevance before prompts see the content.
* **chunkTokenLimit** ensures embeddings stay under the LLM’s budget.

***

## Pattern 4 – Conversations that Stay in Bounds

```typescript
export async function startSupportConversation(
  specificationId: string,
  customerName: string, // e.g., "Acme Corp"
) {
  // In production: query for resources by name (or pass IDs from your database)
  const collections = await graphlit.queryCollections({
    filter: { name: 'Support Documents' }
  });
  const supportCollectionId = collections.collections?.results?.[0]?.id;

  const observables = await graphlit.queryObservables({
    filter: {
      types: [ObservableTypes.Organization],
      name: customerName
    }
  });
  const customerId = observables.observables?.results?.[0]?.id;

  const conversation = await graphlit.createConversation({
    name: `${customerName} Support Triage`,
    specification: { id: specificationId },
    filter: {
      collections: supportCollectionId ? [{ id: supportCollectionId }] : undefined,
      observations: customerId
        ? [
            {
              type: ObservableTypes.Organization,
              observable: { id: customerId },
            },
          ]
        : undefined,
    },
  });

  const response = await graphlit.promptConversation(
    `Summarize the last three authentication incidents for ${customerName} and list current blockers.`,
    conversation.createConversation.id,
  );

  console.log(response.promptConversation.message?.message);
  return conversation.createConversation.id;
}
```

The conversation never sees content outside the selected collection or organization. Tenants stay isolated without writing manual guardrails.

***

## Pattern 5 – Keep the Window Fresh

### Quickly Re-run with Updated Filters

```typescript
export async function rehydrateConversation(
  conversationId: string,
  collectionId: string,
) {
  await graphlit.updateConversation({
    id: conversationId,
    filter: {
      collections: [{ id: collectionId }],
    },
  });

  return graphlit.promptConversation(
    'What changed since our last sync?',
    conversationId,
    undefined,
    undefined,
    undefined,
    undefined,
    undefined,
    'Only report new incidents or updates since the previous answer.',
  );
}
```

### Poll for Newly Processed Content Before Prompting

```typescript
export async function waitForWorkflow(ids: string[]) {
  for (const id of ids) {
    let done = false;
    while (!done) {
      const status = await graphlit.isContentDone(id);
      done = Boolean(status.isContentDone?.result);
      if (!done) await new Promise((resolve) => setTimeout(resolve, 1500));
    }
  }
}
```

Ensuring ingestion is finished before prompting avoids stale context and reduces retries.

***

## Production Checklist

* **Persist IDs**: Store specification, workflow, collection, and entity IDs in configuration so every service call reuses the exact same context rules.
* **Log retrieval metadata**: Inspect `response.promptConversation.details?.sources` to validate which documents powered answers.
* **Segment tenants early**: Collections or feeds per tenant keep context surfaces clean and simplify billing/quotas.
* **Tune once**: Adjust retrieval/reranking directly on the specification instead of ad-hoc prompt engineering.
* **Fallbacks**: Provide multiple specifications via `fallbacks` if different models handle niche cases better (e.g., legal vs. product queries).

***

## Next Steps

* [**Knowledge Graph Tutorial**](/tutorials/knowledge-graph.md) – capture richer entities and relationships to feed your context filters.
* Try the [Next.js chat-graph sample](https://github.com/graphlit/graphlit-samples/tree/main/nextjs/chat-graph) to see the same configuration running in a UI with streaming responses.

***

Build context that respects your product and your customers. Build with Graphlit.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.graphlit.dev/tutorials/context-engineering.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
