# 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](https://docs.graphlit.dev/getting-started/quickstart)
* 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**](https://docs.graphlit.dev/tutorials/knowledge-graph) – 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.
