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


Prerequisites

  • Complete the Quickstart tutorial

  • Graphlit credentials configured in .env (from Getting Started guide)

  • npm install graphlit-client dotenv

Need Python or .NET samples? Open Ask Graphlit inside the Developer Portal (or visit ask.graphlit.dev) and it will translate every call shown here into your SDK of choice.


Shared Setup

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.

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:

npx tsx scripts/create-context-workflow.ts

Ingest with the Workflow

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

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

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.

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

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

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

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


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

Last updated

Was this helpful?