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
Prerequisites
Complete the Quickstart tutorial
Graphlit credentials configured in
.env(from Getting Started guide)npm install graphlit-client dotenv
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.tsIngest 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.summaryExtracted 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?.sourcesto 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
fallbacksif different models handle niche cases better (e.g., legal vs. product queries).
Next Steps
Knowledge Graph Tutorial – capture richer entities and relationships to feed your context filters.
Try the Next.js chat-graph sample to see the same configuration running in a UI with streaming responses.
Build context that respects your product and your customers. Build with Graphlit.
Last updated
Was this helpful?