---
title: "Stop Stuffing Your System Prompt: Build Scalable Agent Skills in LangGraph"
source: https://pessini.medium.com/stop-stuffing-your-system-prompt-build-scalable-agent-skills-in-langgraph-a9856378e8f6
profile: Default
chars: 30530
paywall_detected: false
downloaded: 2026-06-20
---

Stop Stuffing Your System Prompt: Build Scalable Agent Skills in LangGraph

# Stop Stuffing Your System Prompt: Build Scalable Agent Skills in LangGraph

<div class="e">

<div class="e">

<span class="e"></span>

<div class="section">

<div>

<div class="em fx aeg fz ga gb">

</div>

<div class="gc gd ge gf gg">

<div class="v cf">

<div class="cm bd fo fp fq fr">

<div>

# Stop Stuffing Your System Prompt: Build Scalable Agent Skills in LangGraph

</div>

<div>

## Build modular, observable LangGraph agents that scale in production.

<div>

<div class="speechify-ignore v ct">

<div class="speechify-ignore bd e">

<div class="v hw hx hy hz ia ib ic id ie if ig">

<div class="v j ig">

<div class="v ih">

<div>

<div class="bi" aria-describedby="4" aria-labelledby="4">

<div class="ba" tabindex="-1">

<a href="/?source=post_page---byline--a9856378e8f6---------------------------------------" rel="noopener follow" data-discover="true"></a>

<div class="e ii ij bu ik il">

<div class="e ej">

<img src="https://miro.medium.com/v2/resize:fill:64:64/1*WJBfjnbg5Nr_6W3sOPGHsA.jpeg" class="e fi bu bv bw db" loading="lazy" data-testid="authorPhoto" width="32" height="32" alt="Leandro Pessini" />

<div class="im bu e bv bw em g in fh">

</div>

</div>

</div>

</div>

</div>

</div>

</div>

<span class="bb b bc u bg"></span>

<div class="io v j">

<div class="v j ip">

<div class="v j">

<div>

<div class="bi" aria-describedby="5" aria-labelledby="5">

<div class="ba" tabindex="-1">

<span class="bb b bc u bg"><a href="/?source=post_page---byline--a9856378e8f6---------------------------------------" class="z ab ac ey af ag ah ai aj ak al am an iq" data-testid="authorName" rel="noopener follow" data-discover="true">Leandro Pessini</a></span>

</div>

</div>

</div>

</div>

<div class="ir bi">

</div>

<div class="bi">

<span class="bb b bc u bg bd"><span class="bi aem">Follow</span></span>

</div>

</div>

</div>

</div>

<div class="v j is">

<span class="bb b bc u eb"></span>

<div class="v y">

<span testid="storyReadTime">17 min read</span>

<div class="it iu e" aria-hidden="true">

<span class="e" aria-hidden="true"><span class="bb b bc u eb">·</span></span>

</div>

<span testid="storyPublishDate">Feb 16, 2026</span>

</div>

</div>

</div>

<div class="v ct iv iw ix iy iz ja jb jc jd je jf jg jh ji jj jk">

<div class="au bt p ew ex j">

<div class="v j">

<div class="ka e">

<div class="v j kb kc">

<div class="pw-multi-vote-icon ej kd ke kf kg">

<div>

<div>

<div class="bi" aria-describedby="147" aria-labelledby="147">

<div class="ba" tabindex="-1">

![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld2JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsPSJjbGFwIj48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMS4zNy44MjggMTIgMy4yODJsLjYzLTIuNDU0ek0xMy45MTYgMy45NTNsMS41MjMtMi4xMTItMS4xODQtLjM5ek04LjU4OSAxLjg0bDEuNTIyIDIuMTEyLS4zMzctMi41MDF6TTE4LjUyMyAxOC45MmMtLjg2Ljg2LTEuNzUgMS4yNDYtMi42MiAxLjMzYTYgNiAwIDAgMCAuNDA3LS4zNzJjMi4zODgtMi4zODkgMi44Ni00Ljk1MSAxLjM5OS03LjYyM2wtLjkxMi0xLjYwMy0uNzktMS42NzJjLS4yNi0uNTYtLjE5NC0uOTguMjAzLTEuMjg4YS43LjcgMCAwIDEgLjU0Ni0uMTMyYy4yODMuMDQ2LjU0Ni4yMzEuNzI4LjVsMi4zNjMgNC4xNTdjLjk3NiAxLjYyNCAxLjE0MSA0LjIzNy0xLjMyNCA2LjcwMm0tMTAuOTk5LS40MzhMMy4zNyAxNC4zMjhhLjgyOC44MjggMCAwIDEgLjU4NS0xLjQwOC44My44MyAwIDAgMSAuNTg1LjI0MmwyLjE1OCAyLjE1N2EuMzY1LjM2NSAwIDAgMCAuNTE2LS41MTZsLTIuMTU3LTIuMTU4LTEuNDQ5LTEuNDQ5YS44MjYuODI2IDAgMCAxIDEuMTY3LTEuMTdsMy40MzggMy40NGEuMzYzLjM2MyAwIDAgMCAuNTE2IDAgLjM2NC4zNjQgMCAwIDAgMC0uNTE2TDUuMjkzIDkuNTEzbC0uOTctLjk3YS44MjYuODI2IDAgMCAxIDAtMS4xNjYuODQuODQgMCAwIDEgMS4xNjcgMGwuOTcuOTY4IDMuNDM3IDMuNDM2YS4zNi4zNiAwIDAgMCAuNTE3IDAgLjM2Ni4zNjYgMCAwIDAgMC0uNTE2TDYuOTc3IDcuODNhLjgyLjgyIDAgMCAxLS4yNDEtLjU4NC44Mi44MiAwIDAgMSAuODI0LS44MjZjLjIxOSAwIC40My4wODcuNTg0LjI0Mmw1Ljc4NyA1Ljc4N2EuMzY2LjM2NiAwIDAgMCAuNTg3LS40MTVsLTEuMTE3LTIuMzYzYy0uMjYtLjU2LS4xOTQtLjk4LjIwNC0xLjI4OWEuNy43IDAgMCAxIC41NDYtLjEzMmMuMjgzLjA0Ni41NDUuMjMyLjcyNy41MDFsMi4xOTMgMy44NmMxLjMwMiAyLjM4Ljg4MyA0LjU5LTEuMjc3IDYuNzUtMS4xNTYgMS4xNTYtMi42MDIgMS42MjctNC4xOSAxLjM2Ny0xLjQxOC0uMjM2LTIuODY2LTEuMDMzLTQuMDc5LTIuMjQ2TTEwLjc1IDUuOTcxbDIuMTIgMi4xMmMtLjQxLjUwMi0uNDY1IDEuMTctLjEyOCAxLjg5bC4yMi40NjUtMy41MjMtMy41MjNhLjguOCAwIDAgMS0uMDk3LS4zNjhjMC0uMjIuMDg2LS40MjguMjQxLS41ODRhLjg0Ny44NDcgMCAwIDEgMS4xNjcgMG03LjM1NSAxLjcwNWMtLjMxLS40NjEtLjc0Ni0uNzU4LTEuMjMtLjgzN2ExLjQ0IDEuNDQgMCAwIDAtMS4xMS4yNzVjLS4zMTIuMjQtLjUwNS41NDMtLjU5Ljg4MWExLjc0IDEuNzQgMCAwIDAtLjkwNi0uNDY1IDEuNDcgMS40NyAwIDAgMC0uODIuMTA2bC0yLjE4Mi0yLjE4MmExLjU2IDEuNTYgMCAwIDAtMi4yIDAgMS41NCAxLjU0IDAgMCAwLS4zOTYuNzAxIDEuNTYgMS41NiAwIDAgMC0yLjIxLS4wMSAxLjU1IDEuNTUgMCAwIDAtLjQxNi43NTNjLS42MjQtLjYyNC0xLjY0OS0uNjI0LTIuMjM3LS4wMzdhMS41NTcgMS41NTcgMCAwIDAgMCAyLjJjLS4yMzkuMS0uNTAxLjIzOC0uNzE1LjQ1M2ExLjU2IDEuNTYgMCAwIDAgMCAyLjJsLjUxNi41MTVhMS41NTYgMS41NTYgMCAwIDAtLjc1MyAyLjYxNUw3LjAxIDE5YzEuMzIgMS4zMTkgMi45MDkgMi4xODkgNC40NzUgMi40NDlxLjQ4Mi4wOC45NzEuMDhjLjg1IDAgMS42NTMtLjE5OCAyLjM5My0uNTc5LjIzMS4wMzMuNDYuMDU0LjY4Ni4wNTQgMS4yNjYgMCAyLjQ1Ny0uNTIgMy41MDUtMS41NjcgMi43NjMtMi43NjMgMi41NTItNS43MzQgMS40MzktNy41ODZ6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIC8+PC9zdmc+)

</div>

</div>

</div>

</div>

</div>

<div class="pw-multi-vote-count e kr ks kt ku kv kw kx">

<div>

<div class="bi" aria-describedby="148" aria-labelledby="148">

<div class="ba" tabindex="-1">

16<span class="e au ue uf ug uh"></span>

</div>

</div>

</div>

</div>

</div>

</div>

<div class="ky kz e">

<div>

<div class="bi" aria-describedby="6" aria-labelledby="6">

<div class="ba" tabindex="-1">

<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld2JveD0iMCAwIDI0IDI0IiBjbGFzcz0ibGIiPjxwYXRoIGQ9Ik0xOC4wMDYgMTYuODAzYzEuNTMzLTEuNDU2IDIuMjM0LTMuMzI1IDIuMjM0LTUuMzIxQzIwLjI0IDcuMzU3IDE2LjcwOSA0IDEyLjE5MSA0UzQgNy4zNTcgNCAxMS40ODJjMCA0LjEyNiAzLjY3NCA3LjQ4MiA4LjE5MSA3LjQ4Mi44MTcgMCAxLjYyMi0uMTExIDIuMzkzLS4zMjcuMjMxLjIuNDguMzkxLjc0NC41NTkgMS4wNi42OTMgMi4yMDMgMS4wNDQgMy4zOTkgMS4wNDQuMjI0LS4wMDguNC0uMTEyLjQ4Ni0uMjg3YS40OS40OSAwIDAgMC0uMDQyLS41MThjLS40OTUtLjY3LS44NDUtMS4zNjQtMS4wNC0yLjA1N2E0IDQgMCAwIDEtLjEyNS0uNTk4em0tMy4xMjIgMS4wNTUtLjA2Ny0uMjIzLS4zMTUuMDk2YTggOCAwIDAgMS0yLjMxMS4zMzhjLTQuMDIzIDAtNy4yOTItMi45NTUtNy4yOTItNi41ODcgMC0zLjYzMyAzLjI2OS02LjU4OCA3LjI5Mi02LjU4OCA0LjAxNCAwIDcuMTEyIDIuOTU4IDcuMTEyIDYuNTkzIDAgMS43OTQtLjYwOCAzLjQ2OS0yLjAyNyA0LjcybC0uMTk1LjE2OHYuMjU1YzAgLjA1NiAwIC4xNTEuMDE2LjI5NS4wMjUuMjMxLjA4MS40NzguMTU0LjczMy4xNTQuNTU4LjM5OCAxLjExNy43MjIgMS42NTlhNS4zIDUuMyAwIDAgMS0yLjE2NS0uODQ1Yy0uMjc2LS4xNzYtLjcxNC0uMzgzLS45NDEtLjU5eiIgLz48L3N2Zz4=" class="lb" />

<span class="pw-responses-count la lb">1</span>

</div>

</div>

</div>

</div>

<div class="v j eb">

<div class="bi">

<div>

<div class="bi" aria-describedby="7" aria-labelledby="7">

<div class="ba" tabindex="-1">

<div class="bm li e ej">

<div class="bm li e">

![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Ym94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgcHJlc2VydmVhc3BlY3RyYXRpbz0ieE1pZFlNaWQgbWVldCIgc3R5bGU9IndpZHRoOiAxMDAlOyBoZWlnaHQ6IDEwMCU7IHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMHB4LCAwcHgsIDBweCk7IGNvbnRlbnQtdmlzaWJpbGl0eTogdmlzaWJsZTsiPjxkZWZzPjxjbGlwcGF0aCBpZD0iX19sb3R0aWVfZWxlbWVudF8yIj48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHg9IjAiIHk9IjAiIC8+PC9jbGlwcGF0aD48L2RlZnM+PGcgY2xpcC1wYXRoPSJ1cmwoI19fbG90dGllX2VsZW1lbnRfMikiPjxnIHN0eWxlPSJkaXNwbGF5OiBub25lOyI+PGc+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsLW9wYWNpdHk9IjAiIC8+PC9nPjwvZz48ZyBzdHlsZT0iZGlzcGxheTogbm9uZTsiPjxnPjxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbC1vcGFjaXR5PSIwIiAvPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwwLDApIiBvcGFjaXR5PSIxIiBzdHlsZT0iZGlzcGxheTogYmxvY2s7Ij48ZyBvcGFjaXR5PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwxLDAsMCkiPjxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbC1vcGFjaXR5PSIwIiBzdHJva2U9InJnYigxMjgsMTI4LDEyOCkiIHN0cm9rZS1vcGFjaXR5PSIxIiBzdHJva2Utd2lkdGg9IjEiIGQ9IiBNMTkuMzUyODk5NTUxMzkxNiwxOSBDMTkuMzUyODk5NTUxMzkxNiwxOSAyMiwxNi4yNzI2OTkzNTYwNzkxIDIyLDE2LjI3MjY5OTM1NjA3OTEiIC8+PC9nPjwvZz48ZyB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwxLDAsMCkiIG9wYWNpdHk9IjEiIHN0eWxlPSJkaXNwbGF5OiBibG9jazsiPjxnIG9wYWNpdHk9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSI+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsLW9wYWNpdHk9IjAiIHN0cm9rZT0icmdiKDEyOCwxMjgsMTI4KSIgc3Ryb2tlLW9wYWNpdHk9IjEiIHN0cm9rZS13aWR0aD0iMSIgZD0iIE0xOS4zNTI4OTk1NTEzOTE2LDE5IEMxOS4zNTI4OTk1NTEzOTE2LDE5IDE2LjcwNTkwMDE5MjI2MDc0MiwxNi4yNzI2OTkzNTYwNzkxIDE2LjcwNTkwMDE5MjI2MDc0MiwxNi4yNzI2OTkzNTYwNzkxIiAvPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwwLDApIiBvcGFjaXR5PSIxIiBzdHlsZT0iZGlzcGxheTogYmxvY2s7Ij48ZyBvcGFjaXR5PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwxLDAsMCkiPjxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbC1vcGFjaXR5PSIwIiBzdHJva2U9InJnYigxMjgsMTI4LDEyOCkiIHN0cm9rZS1vcGFjaXR5PSIxIiBzdHJva2Utd2lkdGg9IjEiIGQ9IiBNNC42NDcwOTk5NzE3NzEyNCw1IEM0LjY0NzA5OTk3MTc3MTI0LDUgNy4yOTQwOTk4MDc3MzkyNTgsNy43MjczMDAxNjcwODM3NCA3LjI5NDA5OTgwNzczOTI1OCw3LjcyNzMwMDE2NzA4Mzc0IiAvPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwwLDApIiBvcGFjaXR5PSIxIiBzdHlsZT0iZGlzcGxheTogYmxvY2s7Ij48ZyBvcGFjaXR5PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwxLDAsMCkiPjxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbC1vcGFjaXR5PSIwIiBzdHJva2U9InJnYigxMjgsMTI4LDEyOCkiIHN0cm9rZS1vcGFjaXR5PSIxIiBzdHJva2Utd2lkdGg9IjEiIGQ9IiBNNC42NDcwOTk5NzE3NzEyNCw1IEM0LjY0NzA5OTk3MTc3MTI0LDUgMiw3LjcyNzMwMDE2NzA4Mzc0IDIsNy43MjczMDAxNjcwODM3NCIgLz48L2c+PC9nPjxnIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSIgb3BhY2l0eT0iMSIgc3R5bGU9ImRpc3BsYXk6IGJsb2NrOyI+PGcgb3BhY2l0eT0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwwLDApIj48cGF0aCBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGwtb3BhY2l0eT0iMCIgc3Ryb2tlPSJyZ2IoMTI4LDEyOCwxMjgpIiBzdHJva2Utb3BhY2l0eT0iMSIgc3Ryb2tlLXdpZHRoPSIxIiBkPSIgTTExLDQuNzcyNjk5ODMyOTE2MjYgQzExLjE0NDEwMDE4OTIwODk4NCw0Ljc3MjY5OTgzMjkxNjI2IDExLjU3NjQ5OTkzODk2NDg0NCw0Ljc3MjY5OTgzMjkxNjI2IDExLjg2NDgwMDQ1MzE4NjAzNSw0Ljc3MjY5OTgzMjkxNjI2IEMxMi4xNTMxMDAwMTM3MzI5MSw0Ljc3MjY5OTgzMjkxNjI2IDEyLjQ0MTIwMDI1NjM0NzY1Niw0Ljc3MjY5OTgzMjkxNjI2IDEyLjcyOTQ5OTgxNjg5NDUzMSw0Ljc3MjY5OTgzMjkxNjI2IEMxMy4wMTc4MDAzMzExMTU3MjMsNC43NzI2OTk4MzI5MTYyNiAxMy4zMDU5OTk3NTU4NTkzNzUsNC43NzI2OTk4MzI5MTYyNiAxMy41OTQzMDAyNzAwODA1NjYsNC43NzI2OTk4MzI5MTYyNiBDMTMuODgyNTk5ODMwNjI3NDQxLDQuNzcyNjk5ODMyOTE2MjYgMTQuMTcwODAwMjA5MDQ1NDEsNC43NzI2OTk4MzI5MTYyNiAxNC40NTkwOTk3Njk1OTIyODUsNC43NzI2OTk4MzI5MTYyNiBDMTQuNzQ3NDAwMjgzODEzNDc3LDQuNzcyNjk5ODMyOTE2MjYgMTUuMDM1OTAwMTE1OTY2Nzk3LDQuNzY3MDAwMTk4MzY0MjU4IDE1LjMyMzkwMDIyMjc3ODMyLDQuNzcyNjk5ODMyOTE2MjYgQzE1LjYxMTkwMDMyOTU4OTg0NCw0Ljc3ODM5OTk0NDMwNTQyIDE1LjkwNDUwMDAwNzYyOTM5NSw0Ljc2MzI5OTk0MjAxNjYwMiAxNi4xODcwMDAyNzQ2NTgyMDMsNC44MDY3OTk4ODg2MTA4NCBDMTYuNDY5NDk5NTg4MDEyNjk1LDQuODUwMjk5ODM1MjA1MDc4IDE2Ljc1NTE5OTQzMjM3MzA0Nyw0LjkyNTM5OTc4MDI3MzQzNzUgMTcuMDE5MTAwMTg5MjA4OTg0LDUuMDMzODk5Nzg0MDg4MTM1IEMxNy4yODMwMDA5NDYwNDQ5MjIsNS4xNDIzOTk3ODc5MDI4MzIgMTcuNTQwMDAwOTE1NTI3MzQ0LDUuMjg5MTAwMTcwMTM1NDk4IDE3Ljc3MDUwMDE4MzEwNTQ3LDUuNDU3Nzk5OTExNDk5MDIzIEMxOC4wMDA5OTk0NTA2ODM1OTQsNS42MjY1MDAxMjk2OTk3MDcgMTguMjE1OTk5NjAzMjcxNDg0LDUuODI4NzAwMDY1NjEyNzkzIDE4LjQwMTg5OTMzNzc2ODU1NSw2LjA0NTgwMDIwOTA0NTQxIEMxOC41ODc3OTkwNzIyNjU2MjUsNi4yNjI4OTk4NzU2NDA4NjkgMTguNzUyMTk5MTcyOTczNjMzLDYuNTA3NTk5ODMwNjI3NDQxIDE4Ljg4NTkwMDQ5NzQzNjUyMyw2Ljc2MDM5OTgxODQyMDQxIEMxOS4wMTk1OTk5MTQ1NTA3OCw3LjAxMzE5OTgwNjIxMzM3OSAxOS4xMjczMDAyNjI0NTExNzIsNy4yODcwOTk4MzgyNTY4MzYgMTkuMjAzODk5MzgzNTQ0OTIyLDcuNTYyNzk5OTMwNTcyNTEgQzE5LjI4MDUwMDQxMTk4NzMwNSw3LjgzODUwMDAyMjg4ODE4NCAxOS4zMjA0OTk0MjAxNjYwMTYsOC4xMjgzOTk4NDg5Mzc5ODggMTkuMzQ1MzAwNjc0NDM4NDc3LDguNDE0NDAwMTAwNzA4MDA4IEMxOS4zNzAxMDAwMjEzNjIzMDUsOC43MDA0MDAzNTI0NzgwMjcgMTkuMzUxNjAwNjQ2OTcyNjU2LDguOTkwNjk5NzY4MDY2NDA2IDE5LjM1Mjg5OTU1MTM5MTYsOS4yNzg5MDAxNDY0ODQzNzUgQzE5LjM1NDIwMDM2MzE1OTE4LDkuNTY3MDk5NTcxMjI4MDI3IDE5LjM1Mjg5OTU1MTM5MTYsOS44NTU0MDAwODU0NDkyMTkgMTkuMzUyODk5NTUxMzkxNiwxMC4xNDM2OTk2NDU5OTYwOTQgQzE5LjM1Mjg5OTU1MTM5MTYsMTAuNDMyMDAwMTYwMjE3Mjg1IDE5LjM1Mjg5OTU1MTM5MTYsMTAuNzIwMTAwNDAyODMyMDMxIDE5LjM1Mjg5OTU1MTM5MTYsMTEuMDA4Mzk5OTYzMzc4OTA2IEMxOS4zNTI4OTk1NTEzOTE2LDExLjI5NjY5OTUyMzkyNTc4MSAxOS4zNTI4OTk1NTEzOTE2LDExLjU4NDg5OTkwMjM0Mzc1IDE5LjM1Mjg5OTU1MTM5MTYsMTEuODczMjAwNDE2NTY0OTQxIEMxOS4zNTI4OTk1NTEzOTE2LDEyLjE2MTQ5OTk3NzExMTgxNiAxOS4zNTI4OTk1NTEzOTE2LDEyLjQ0OTcwMDM1NTUyOTc4NSAxOS4zNTI4OTk1NTEzOTE2LDEyLjczNzk5OTkxNjA3NjY2IEMxOS4zNTI4OTk1NTEzOTE2LDEzLjAyNjMwMDQzMDI5Nzg1MiAxOS4zNTI4OTk1NTEzOTE2LDEzLjMxNDQ5OTg1NTA0MTUwNCAxOS4zNTI4OTk1NTEzOTE2LDEzLjYwMjgwMDM2OTI2MjY5NSBDMTkuMzUyODk5NTUxMzkxNiwxMy44OTEwOTk5Mjk4MDk1NyAxOS4zNTI4OTk1NTEzOTE2LDEzLjU2Nzk5OTgzOTc4MjcxNSAxOS4zNTI4OTk1NTEzOTE2LDE0LjQ2NzQ5OTczMjk3MTE5MSBDMTkuMzUyODk5NTUxMzkxNiwxNS4zNjY5OTk2MjYxNTk2NjggMTkuMzUyODk5NTUxMzkxNiwxOC4yNDQ2MDAyOTYwMjA1MDggMTkuMzUyODk5NTUxMzkxNiwxOSIgLz48L2c+PC9nPjxnIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSIgb3BhY2l0eT0iMSIgc3R5bGU9ImRpc3BsYXk6IGJsb2NrOyI+PGcgb3BhY2l0eT0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwwLDApIj48cGF0aCBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGwtb3BhY2l0eT0iMCIgc3Ryb2tlPSJyZ2IoMTI4LDEyOCwxMjgpIiBzdHJva2Utb3BhY2l0eT0iMSIgc3Ryb2tlLXdpZHRoPSIxIiBkPSIgTTEzLDE5LjIyNzMwMDY0MzkyMDkgQzEyLjg1NTg5OTgxMDc5MTAxNiwxOS4yMjczMDA2NDM5MjA5IDEyLjQyMzUwMDA2MTAzNTE1NiwxOS4yMjczMDA2NDM5MjA5IDEyLjEzNTE5OTU0NjgxMzk2NSwxOS4yMjczMDA2NDM5MjA5IEMxMS44NDY4OTk5ODYyNjcwOSwxOS4yMjczMDA2NDM5MjA5IDExLjU1ODc5OTc0MzY1MjM0NCwxOS4yMjczMDA2NDM5MjA5IDExLjI3MDUwMDE4MzEwNTQ2OSwxOS4yMjczMDA2NDM5MjA5IEMxMC45ODIxOTk2Njg4ODQyNzcsMTkuMjI3MzAwNjQzOTIwOSAxMC42OTQwMDAyNDQxNDA2MjUsMTkuMjI3MzAwNjQzOTIwOSAxMC40MDU2OTk3Mjk5MTk0MzQsMTkuMjI3MzAwNjQzOTIwOSBDMTAuMTE3NDAwMTY5MzcyNTU5LDE5LjIyNzMwMDY0MzkyMDkgOS44MjkxOTk3OTA5NTQ1OSwxOS4yMjczMDA2NDM5MjA5IDkuNTQwOTAwMjMwNDA3NzE1LDE5LjIyNzMwMDY0MzkyMDkgQzkuMjUyNTk5NzE2MTg2NTIzLDE5LjIyNzMwMDY0MzkyMDkgOC45NjQwOTk4ODQwMzMyMDMsMTkuMjMyOTk5ODAxNjM1NzQyIDguNjc2MDk5Nzc3MjIxNjgsMTkuMjI3MzAwNjQzOTIwOSBDOC4zODgwOTk2NzA0MTAxNTYsMTkuMjIxNTk5NTc4ODU3NDIyIDguMDk1NDk5OTkyMzcwNjA1LDE5LjIzNjcwMDA1Nzk4MzQgNy44MTMwMDAyMDIxNzg5NTUsMTkuMTkzMTk5MTU3NzE0ODQ0IEM3LjUzMDQ5OTkzNTE1MDE0NjUsMTkuMTQ5NzAwMTY0Nzk0OTIyIDcuMjQ0ODAwMDkwNzg5Nzk1LDE5LjA3NDYwMDIxOTcyNjU2MiA2Ljk4MDg5OTgxMDc5MTAxNiwxOC45NjYxMDA2OTI3NDkwMjMgQzYuNzE3MDAwMDA3NjI5Mzk0NSwxOC44NTc1OTkyNTg0MjI4NSA2LjQ2MDAwMDAzODE0Njk3MywxOC43MTA4OTkzNTMwMjczNDQgNi4yMjk0OTk4MTY4OTQ1MzEsMTguNTQyMjAwMDg4NTAwOTc3IEM1Ljk5OTAwMDA3MjQ3OTI0OCwxOC4zNzM1MDA4MjM5NzQ2MSA1Ljc4Mzk5OTkxOTg5MTM1NywxOC4xNzEzMDA4ODgwNjE1MjMgNS41OTgxMDAxODUzOTQyODcsMTcuOTU0MjAwNzQ0NjI4OTA2IEM1LjQxMjE5OTk3NDA2MDA1OSwxNy43MzcxMDA2MDExOTYyOSA1LjI0Nzc5OTg3MzM1MjA1MSwxNy40OTIzOTkyMTU2OTgyNDIgNS4xMTQwOTk5Nzk0MDA2MzUsMTcuMjM5NTk5MjI3OTA1MjczIEM0Ljk4MDQwMDA4NTQ0OTIxOSwxNi45ODY3OTkyNDAxMTIzMDUgNC44NzI3MDAyMTQzODU5ODYsMTYuNzEyOTAwMTYxNzQzMTY0IDQuNzk2MTAwMTM5NjE3OTIsMTYuNDM3MjAwNTQ2MjY0NjUgQzQuNzE5NTAwMDY0ODQ5ODUzNSwxNi4xNjE1MDA5MzA3ODYxMzMgNC42Nzk1MDAxMDI5OTY4MjYsMTUuODcxNjAwMTUxMDYyMDEyIDQuNjU0Njk5ODAyMzk4NjgyLDE1LjU4NTU5OTg5OTI5MTk5MiBDNC42Mjk4OTk5Nzg2Mzc2OTUsMTUuMjk5NTk5NjQ3NTIxOTczIDQuNjQ4Mzk5ODI5ODY0NTAyLDE1LjAwOTMwMDIzMTkzMzU5NCA0LjY0NzA5OTk3MTc3MTI0LDE0LjcyMTA5OTg1MzUxNTYyNSBDNC42NDU4MDAxMTM2Nzc5Nzg1LDE0LjQzMjkwMDQyODc3MTk3MyA0LjY0NzA5OTk3MTc3MTI0LDE0LjE0NDU5OTkxNDU1MDc4MSA0LjY0NzA5OTk3MTc3MTI0LDEzLjg1NjMwMDM1NDAwMzkwNiBDNC42NDcwOTk5NzE3NzEyNCwxMy41Njc5OTk4Mzk3ODI3MTUgNC42NDcwOTk5NzE3NzEyNCwxMy4yNzk4OTk1OTcxNjc5NjkgNC42NDcwOTk5NzE3NzEyNCwxMi45OTE2MDAwMzY2MjEwOTQgQzQuNjQ3MDk5OTcxNzcxMjQsMTIuNzAzMzAwNDc2MDc0MjE5IDQuNjQ3MDk5OTcxNzcxMjQsMTIuNDE1MTAwMDk3NjU2MjUgNC42NDcwOTk5NzE3NzEyNCwxMi4xMjY3OTk1ODM0MzUwNTkgQzQuNjQ3MDk5OTcxNzcxMjQsMTEuODM4NTAwMDIyODg4MTg0IDQuNjQ3MDk5OTcxNzcxMjQsMTEuNTUwMjk5NjQ0NDcwMjE1IDQuNjQ3MDk5OTcxNzcxMjQsMTEuMjYyMDAwMDgzOTIzMzQgQzQuNjQ3MDk5OTcxNzcxMjQsMTAuOTczNjk5NTY5NzAyMTQ4IDQuNjQ3MDk5OTcxNzcxMjQsMTAuNjg1NTAwMTQ0OTU4NDk2IDQuNjQ3MDk5OTcxNzcxMjQsMTAuMzk3MTk5NjMwNzM3MzA1IEM0LjY0NzA5OTk3MTc3MTI0LDEwLjEwODkwMDA3MDE5MDQzIDQuNjQ3MDk5OTcxNzcxMjQsMTAuNDMyMDAwMTYwMjE3Mjg1IDQuNjQ3MDk5OTcxNzcxMjQsOS41MzI1MDAyNjcwMjg4MDkgQzQuNjQ3MDk5OTcxNzcxMjQsOC42MzMwMDAzNzM4NDAzMzIgNC42NDcwOTk5NzE3NzEyNCw1Ljc1NTQwMDE4MDgxNjY1IDQuNjQ3MDk5OTcxNzcxMjQsNSIgLz48L2c+PC9nPjwvZz48L3N2Zz4=)

</div>

</div>

</div>

</div>

</div>

</div>

<div class="la e">

</div>

</div>

</div>

</div>

<div class="v j jl jm jn jo jp jq jr js jt ju jv jw jx jy jz">

<div class="lj bt by r s">

</div>

<div class="au bt">

<div>

<div class="bi" aria-describedby="8" aria-labelledby="8">

<div class="ba" tabindex="-1">

<div class="bi">

<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgdmlld2JveD0iMCAwIDI0IDI0IiBjbGFzcz0ibG8iPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik0xNy41IDEuMjVhLjUuNSAwIDAgMSAxIDB2Mi41SDIxYS41LjUgMCAwIDEgMCAxaC0yLjV2Mi41YS41LjUgMCAwIDEtMSAwdi0yLjVIMTVhLjUuNSAwIDAgMSAwLTFoMi41em0tMTEgNC41YTEgMSAwIDAgMSAxLTFIMTFhLjUuNSAwIDAgMCAwLTFINy41YTIgMiAwIDAgMC0yIDJ2MTRhLjUuNSAwIDAgMCAuOC40bDUuNy00LjQgNS43IDQuNGEuNS41IDAgMCAwIC44LS40di04LjVhLjUuNSAwIDAgMC0xIDB2Ny40OGwtNS4yLTRhLjUuNSAwIDAgMC0uNiAwbC01LjIgNHoiIC8+PC9zdmc+" class="lo" />

</div>

</div>

</div>

</div>

</div>

<div class="fi lp cr">

<div class="e y">

<div class="v cf">

<div class="lq lr ls lt lu lv cm bd">

<div class="v">

<div>

<a href="https://medium.com/plans?dimension=post_audio_button&amp;postId=a9856378e8f6&amp;source=upgrade_membership---post_audio_button-----------------------------------------" class="z ab ac ey af ag ah ai aj ak al am an ao ap" rel="noopener follow"></a>

<div>

<div class="bi" aria-describedby="9" aria-labelledby="9">

<div class="ba" tabindex="-1">

![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgdmlld2JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMgMTJhOSA5IDAgMSAxIDE4IDAgOSA5IDAgMCAxLTE4IDBtOS0xMEM2LjQ3NyAyIDIgNi40NzcgMiAxMnM0LjQ3NyAxMCAxMCAxMCAxMC00LjQ3NyAxMC0xMFMxNy41MjMgMiAxMiAybTMuMzc2IDEwLjQxNi00LjU5OSAzLjA2NmEuNS41IDAgMCAxLS43NzctLjQxNlY4LjkzNGEuNS41IDAgMCAxIC43NzctLjQxNmw0LjU5OSAzLjA2NmEuNS41IDAgMCAxIDAgLjgzMiIgY2xpcC1ydWxlPSJldmVub2RkIiAvPjwvc3ZnPg==)

<div class="by r s">

Listen

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

<div class="bi" aria-describedby="postFooterSocialMenu" aria-labelledby="postFooterSocialMenu">

<div>

<div class="bi" aria-describedby="10" aria-labelledby="10">

<div class="ba" tabindex="-1">

![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgdmlld2JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTE1LjIxOCA0LjkzMWEuNC40IDAgMCAxLS4xMTguMTMybC4wMTIuMDA2YS40NS40NSAwIDAgMS0uMjkyLjA3NC41LjUgMCAwIDEtLjMtLjEzbC0yLjAyLTIuMDJ2Ny4wN2MwIC4yOC0uMjMuNS0uNS41cy0uNS0uMjItLjUtLjV2LTcuMDRsLTIgMmEuNDUuNDUgMCAwIDEtLjU3LjA0aC0uMDJhLjQuNCAwIDAgMS0uMTYtLjMuNC40IDAgMCAxIC4xLS4zMmwyLjgtMi44YS41LjUgMCAwIDEgLjcgMGwyLjggMi43OWEuNDIuNDIgMCAwIDEgLjA2OC40OThtLS4xMDYuMTM4LjAwOC4wMDR2LS4wMXpNMTYgNy4wNjNoMS41YTIgMiAwIDAgMSAyIDJ2MTBhMiAyIDAgMCAxLTIgMmgtMTFjLTEuMSAwLTItLjktMi0ydi0xMGEyIDIgMCAwIDEgMi0ySDhhLjUuNSAwIDAgMSAuMzUuMTUuNS41IDAgMCAxIC4xNS4zNS41LjUgMCAwIDEtLjE1LjM1LjUuNSAwIDAgMS0uMzUuMTVINi40Yy0uNSAwLS45LjQtLjkuOXYxMC4yYS45LjkgMCAwIDAgLjkuOWgxMS4yYy41IDAgLjktLjQuOS0uOXYtMTAuMmMwLS41LS40LS45LS45LS45SDE2YS41LjUgMCAwIDEgMC0xIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIC8+PC9zdmc+)

<div class="by r s">

Share

</div>

</div>

</div>

</div>

</div>

<div class="bi">

<div class="bi">

<div>

<div class="bi" aria-describedby="149" aria-labelledby="149">

<div class="ba" tabindex="-1">

![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgdmlld2JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTQuMzg1IDEyYzAgLjU1LjIgMS4wMi41OSAxLjQxLjM5LjQuODYuNTkgMS40MS41OXMxLjAyLS4yIDEuNDEtLjU5Yy40LS4zOS41OS0uODYuNTktMS40MXMtLjItMS4wMi0uNTktMS40MWExLjkzIDEuOTMgMCAwIDAtMS40MS0uNTljLS41NSAwLTEuMDIuMi0xLjQxLjU5LS40LjM5LS41OS44Ni0uNTkgMS40MW01LjYyIDBjMCAuNTUuMiAxLjAyLjU4IDEuNDEuNC40Ljg3LjU5IDEuNDIuNTlzMS4wMi0uMiAxLjQxLS41OWMuNC0uMzkuNTktLjg2LjU5LTEuNDFzLS4yLTEuMDItLjU5LTEuNDFhMS45MyAxLjkzIDAgMCAwLTEuNDEtLjU5Yy0uNTUgMC0xLjAzLjItMS40Mi41OXMtLjU4Ljg2LS41OCAxLjQxbTUuNiAwYzAgLjU1LjIgMS4wMi41OCAxLjQxLjQuNC44Ny41OSAxLjQzLjU5czEuMDMtLjIgMS40Mi0uNTkuNTgtLjg2LjU4LTEuNDEtLjItMS4wMi0uNTgtMS40MWExLjkzIDEuOTMgMCAwIDAtMS40Mi0uNTljLS41NiAwLTEuMDQuMi0xLjQzLjU5cy0uNTguODYtLjU4IDEuNDEiIGNsaXAtcnVsZT0iZXZlbm9kZCIgLz48L3N2Zz4=)

<div class="by r s">

More

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm mn">
<img src="https://miro.medium.com/v2/resize:fit:700/1*bmEnfLZPK4haonUnt19a8Q.jpeg" class="bd lv my mz" loading="eager" role="presentation" width="700" height="383" />
</div>
</div>
</figure>

## Introduction

An agent can have access to many tools. That does not mean it knows how to use them well. It may not know which tool to call first, which parameters actually work, or **what tends to fail in production**.

To address that, I built a skills system for a **LangGraph agent**. The idea is simple: domain knowledge lives outside the prompt and is loaded only when a task requires it. The agent sees a lightweight catalog first and pulls in detailed instructions only when needed.

The approach is inspired by how *Claude Code* structures its skills which follows the <a href="https://agentskills.io/" class="z oy" rel="noopener ugc nofollow" target="_blank">Agent Skills</a> open standard, adapted to LangGraph’s execution model.

**GitHub repo:** <a href="https://github.com/pessini/langgraph-skills-agent" class="z oy" rel="noopener ugc nofollow" target="_blank"><strong>https://github.com/pessini/langgraph-skills-agent</strong></a>

## What Are Skills?

A skill is a unit of domain knowledge stored on disk. In this implementation, it is a folder containing a required `SKILL.md` file and optional supporting resources.

`SKILL.md` has two parts:

1.  <span id="28b5">YAML frontmatter with metadata such as name, description, and tags</span>
2.  <span id="6931">Markdown instructions intended for the LLM</span>

More info can be found in:

<div class="pl pm pn po pp pq">

<a href="https://agentskills.io/what-are-skills?source=post_page-----a9856378e8f6---------------------------------------" rel="noopener  ugc nofollow" target="_blank"></a>

<div class="pr v bx">

<div class="ps v cs cf ca pt">

## What are skills? - Agent Skills

<div class="qb e">

### Agent Skills are a lightweight, open format for extending AI agent capabilities with specialized knowledge and…

</div>

<div class="qc e">

agentskills.io

</div>

</div>

<div class="qd e">

<div class="qe e qf qg qh qd qi lv pq">

</div>

</div>

</div>

</div>

> A skill is not just text. It is a structured unit of domain knowledge that can be identified through metadata and loaded only when required.

</div>

</div>

</div>

<div class="v cf qt qu qv qw" role="separator">

<span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz"></span>

</div>

<div class="gc gd ge gf gg">

<div class="v cf">

<div class="cm bd fo fp fq fr">

## Why Skills Should Be Tool Calls?

A straightforward way to give an agent domain knowledge is to place all skill content directly in the system prompt. Concatenate the files and include them in every request.

That approach works initially, but it has several practical drawbacks.

### Token cost grows with every skill

If you have 10 skills at 2,000 tokens each, that is 20,000 tokens added to every request, including trivial ones. With usage based pricing, this increases cost. With local models, it reduces the effective context window available for actual reasoning.

### Longer prompts reduce signal clarity

As the system prompt grows, relevant instructions compete with unrelated content. Even if all information is present, the model must scan a large block of text to find what applies to the current task. In practice, this can reduce consistency.

### Updates require redeployment

If skills are embedded in the prompt, updating them usually means changing code and redeploying the agent. When skills live as files on disk and are loaded dynamically, updating a markdown file is enough.

### You cannot see which knowledge was used

When all skills are injected into the prompt, they appear as one large text block in traces. It is difficult to determine:

- <span id="e3ab">Which skill was actually relevant;</span>
- <span id="5313">Whether the agent relied on it and;</span>
- <span id="fcfb">When it influenced the response.</span>

With tool calls, you get structured, filterable traces that connect skill loading to downstream actions. If you’re using something like Langfuse, you can correlate skill usage with success rates and build a feedback loop for improving skill content.

The **progressive disclosure** approach solves all four: the agent sees a lightweight catalog (~500 tokens), loads full instructions (~2,000 tokens) only for the skills it needs, and every load is a traceable event.

## An Alternative View: Compressed Prompts Instead of Skills

Not all teams structure agent knowledge this way.

In a recent post, Vercel describes an internal evaluation where a single compressed prompt file outperformed a modular skills setup in their benchmarks.

<div class="pl pm pn po pp pq">

<a href="https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals?source=post_page-----a9856378e8f6---------------------------------------" rel="noopener  ugc nofollow" target="_blank"></a>

<div class="pr v bx">

<div class="ps v cs cf ca pt">

## AGENTS.md outperforms skills in our agent evals - Vercel

<div class="qb e">

### A compressed 8KB docs index in AGENTS.md achieved 100% on Next.js 16 API evals. Skills maxed at 79%. Here's what we…

</div>

<div class="qc e">

vercel.com

</div>

</div>

<div class="qd e">

<div class="ry e qf qg qh qd qi lv pq">

</div>

</div>

</div>

</div>

Their conclusion was that, for their use case, consolidating instructions into one carefully structured prompt led to better results than dynamically loading separate skill files.

> Progressive loading is not automatically superior. In some scenarios, especially when the domain is stable and tightly scoped, a well structured, compact system prompt may perform better and introduce less operational complexity.

There are tradeoffs between the two approaches.

A single compressed prompt:

- <span id="a32d">Reduces tool call overhead</span>
- <span id="b135">Avoids additional latency from skill loading</span>
- <span id="8b55">Keeps all reasoning context in one place</span>

A modular skills system:

- <span id="3231">Separates domain knowledge from the base prompt</span>
- <span id="69dc">Allows independent updates per skill</span>
- <span id="6c37">Makes knowledge usage observable</span>
- <span id="b7c1">Scales better as the number of domains grows</span>

The right choice depends on the problem.

For example, LangChain’s SQL Assistant pattern uses **skills to represent distinct capabilities or personas**. In that case, modularization is useful because each skill corresponds to a different role or tool interaction pattern. The separation is conceptual, not just structural.

<div class="pl pm pn po pp pq">

<a href="https://docs.langchain.com/oss/python/langchain/multi-agent/skills-sql-assistant?source=post_page-----a9856378e8f6---------------------------------------" rel="noopener  ugc nofollow" target="_blank"></a>

<div class="pr v bx">

<div class="ps v cs cf ca pt">

## Build a SQL assistant with on-demand skills - Docs by LangChain

<div class="qb e">

### This tutorial shows how to use progressive disclosure - a context management technique where the agent loads…

</div>

<div class="qc e">

docs.langchain.com

</div>

</div>

<div class="qd e">

<div class="sj e qf qg qh qd qi lv pq">

</div>

</div>

</div>

</div>

In contrast, **if an agent operates in a narrow domain with a small**, stable set of behaviors, **a compressed prompt may be sufficient** and simpler to maintain.

The decision is less about which approach is “better” and more about how much variability, scale, and operational control the system requires.

</div>

</div>

</div>

<div class="v cf qt qu qv qw" role="separator">

<span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz"></span>

</div>

<div class="gc gd ge gf gg">

<div class="v cf">

<div class="cm bd fo fp fq fr">

**Before walking through the implementation, the example in this article use the following public repository as a reference skill set:**

<div class="pl pm pn po pp pq">

<a href="https://github.com/haunchen/n8n-skills/?source=post_page-----a9856378e8f6---------------------------------------" rel="noopener  ugc nofollow" target="_blank"></a>

<div class="pr v bx">

<div class="ps v cs cf ca pt">

## GitHub - haunchen/n8n-skills: Designed specifically for AI assistants, the n8n Workflow Automation…

<div class="qb e">

### Designed specifically for AI assistants, the n8n Workflow Automation Skills Suite. - haunchen/n8n-skills

</div>

<div class="qc e">

github.com

</div>

</div>

<div class="qd e">

<div class="sk e qf qg qh qd qi lv pq">

</div>

</div>

</div>

</div>

## The Implementation

The system has four main parts: a skill store, two tools the LLM can call, a system prompt, and a LangGraph execution loop. Instead of walking through each file, this section focuses on the design decisions behind it.

### Agent structure

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm sl">
<img src="https://miro.medium.com/v2/resize:fit:700/1*LlZ1KgY5uamlvU-nSY3rFQ.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="242" />
</div>
</div>
<figcaption>Directory Tree Structure for the Agent</figcaption>
</figure>

**GitHub repo:** <a href="https://github.com/pessini/langgraph-skills-agent" class="z oy" rel="noopener ugc nofollow" target="_blank"><strong>https://github.com/pessini/langgraph-skills-agent</strong></a>

### Three Tiers of Knowledge

Knowledge is exposed to the agent in three layers.

**Tier 1 — Catalog (always present)**

The system prompt includes a structured list of available skills. It contains the name, description, tags, and the names of supporting files. This is small enough to include in every request.

``` mo
<skill>
  <name>n8n-skills</name>
  <description>n8n workflow automation knowledge base. Provides n8n node
    information, workflow patterns, and configuration examples.</description>
  <tags>n8n, workflow, automation</tags>
  <supporting_files>resources/guides/usage-guide.md, resources/guides/how-to-find-nodes.md</supporting_files>
</skill>
```

The structured format makes it easier for the model to identify valid skill names and available references before loading anything.

**Tier 2 — Skill Instructions (on demand)**

When the model calls `load_skill("n8n-skills")`, it receives the full `SKILL.md` content along with the list of supporting files. This includes workflow patterns, configuration guidance, and common constraints.

**Tier 3 — Supporting Files (fine grained)**

If needed, the model can request a specific file using `read_skill_file`. This allows it to load only the relevant reference material instead of the entire folder.

**In practice, this means the amount of loaded knowledge depends on the task**. A simple greeting loads only the catalog. A workflow question loads the skill instructions. More detailed tasks may load additional reference files.

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm sz">
<img src="https://miro.medium.com/v2/resize:fit:700/1*XnuhJOvqSJZiQeOH8Ao6jw.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="208" />
</div>
</div>
</figure>

### The Skill Store

`SkillStore` handles discovery and caching. It maintains two caches:

1.  <span id="5b87">Metadata cache for building the catalog;</span>
2.  <span id="9cac">Content cache for full skill instructions.</span>

``` mo
class SkillStore:
    def __init__(self, skills_dir: str | Path) -> None:
        self.skills_dir = Path(skills_dir)
        self._metadata_cache: dict[str, SkillMetadata] = {}
        self._content_cache: dict[str, ParsedSkill] = {}
        self._scanned = False

    def scan(self) -> dict[str, SkillMetadata]:
        """Discover all skills - parse only YAML frontmatter (fast)."""
        if self._scanned and self._metadata_cache:
            return self._metadata_cache

        self._metadata_cache.clear()
        self._content_cache.clear()

        for skill_file in self.skills_dir.rglob("SKILL.md"):
            try:
                metadata = parse_metadata_only(skill_file)
                skill_name = metadata.name if metadata.name != "unknown" else skill_file.parent.name
                metadata.path = skill_file
                self._metadata_cache[skill_name] = metadata
            except Exception as e:
                logger.warning(f"Failed to parse skill at {skill_file}: {e}")

        self._scanned = True
        return self._metadata_cache

    def load(self, skill_name: str) -> ParsedSkill | None:
        """Lazy-load the full SKILL.md content for a skill."""
        if skill_name in self._content_cache:
            return self._content_cache[skill_name]

        metadata = self._metadata_cache.get(skill_name)
        if not metadata or not metadata.path:
            return None

        parsed = parse_skill_file(metadata.path)
        self._content_cache[skill_name] = parsed
        return parsed
```

`scan()` parses only the YAML frontmatter. The markdown body is not read at this stage, which keeps discovery fast even with many skills.

If a skill file is malformed, it is skipped and logged instead of stopping the entire scan.

`load()` reads and caches the full skill content on first access. Subsequent calls return the cached version.

The metadata and full content are represented by separate dataclasses:

``` mo
@dataclass
class SkillMetadata:
    """Lightweight metadata extracted from YAML frontmatter."""
    name: str
    description: str
    version: str = "1.0"
    tags: list[str] = field(default_factory=list)
    dependencies: list[str] = field(default_factory=list)
    path: Path | None = None

@dataclass
class ParsedSkill:
    """Full skill - metadata plus the markdown body."""
    metadata: SkillMetadata
    content: str
```

Splitting these types allows catalog generation to remain lightweight without touching large markdown files.

### The Skill Tools

The LLM interacts with the store through two tools: `load_skill` and `read_skill_file`.

``` mo
def create_skill_tools(store: SkillStore) -> list[BaseTool]:
    available = ", ".join(store.get_skill_names()) or "none"

    @tool
    def load_skill(skill_name: str) -> str:
        """Load expert knowledge for a skill."""
        parsed = store.load(skill_name)
        if parsed is None:
            names = ", ".join(store.get_skill_names())
            return json.dumps(
                {"error": f"Skill '{skill_name}' not found", "available_skills": names}
            )
        return json.dumps({
            "skill_name": skill_name,
            "description": parsed.metadata.description,
            "instructions": parsed.content,
            "available_files": store.list_supporting_files(skill_name),
        })

    @tool
    def read_skill_file(skill_name: str, filename: str) -> str:
        """Read a supporting file from a skill folder."""
        try:
            content = store.read_supporting_file(skill_name, filename)
            return json.dumps({
                "skill_name": skill_name, "filename": filename, "content": content,
            })
        except (FileNotFoundError, ValueError) as e:
            return json.dumps({"error": f"Error reading file: {e}"})

    # Dynamic docstrings - the LLM sees these as part of the tool schema
    load_skill.__doc__ = (
        f"Load expert knowledge for a skill. Returns JSON with the skill's "
        f"instructions and a list of available supporting files.\n\n"
        f"Available skills: {available}\n\n"
        f"Args:\n    skill_name: Exact name of the skill to load."
    )

    return [load_skill, read_skill_file]
```

A few implementation details matter:

**Dynamic docstrings:** used to inject the list of valid skill names into the tool schema. This reduces invalid skill name calls.

**Self-correcting errors:** when `load_skill` fails, the error response includes valid skill names so the model can retry with a correct value.

**Directory traversal protection:** `read_skill_file` validates paths to prevent directory traversal. Since the model controls the filename argument, path validation is necessary once the system is exposed to external input.

### The System Prompt and Graph

The system prompt explains how the model should interact with the skills system. It describes what a skill is, which tools are available, and the expected order of operations.

``` mo
SYSTEM_PROMPT = """You are a helpful AI assistant with access to a skills system
that provides domain expertise.

Current time: {current_time}

<skills_system>

## What are Skills?

Skills are packages of domain expertise that extend your capabilities. Each skill
contains:
- **Instructions**: Detailed guidance on when and how to apply the skill
- **Supporting files**: Reference documentation, cheatsheets, and examples

## How to Use Skills

**Skill names are NOT callable functions.** You MUST use the skill access tools:

1. `load_skill(skill_name)` — Load the full instructions for a skill
2. `read_skill_file(skill_name, filename)` — Access a specific supporting file

## Progressive Discovery Workflow

1. **Browse**: Review the skill summaries below
2. **Match**: When a task aligns with a skill's description, load that skill first
3. **Load**: Call `load_skill(skill_name)` to get detailed instructions
4. **Inspect**: Check `available_files` in the response
5. **Reference**: Use `read_skill_file` for specific documentation as needed
6. **Execute**: Apply the skill's guidance to complete the user's task

## Available Skills

{skill_catalog}

</skills_system>

## Guidelines

- For general conversation, respond directly without loading skills.
- For domain-specific tasks that match a skill's description, load the relevant
  skill(s) first.
- Execute first, explain second — take action before narrating.

## Task Completion

After completing the user's request:
1. Stop calling tools immediately
2. Summarize what was accomplished
3. Provide relevant details (next steps, notes)

**IMPORTANT**: Do NOT repeatedly call the same tool. If a tool succeeds, you are done.
```

> **Note:** This prompt serves as a baseline. In production, you may introduce stricter routing rules and domain-specific guidance to meet compliance, security, and organizational requirements.

At runtime, `{skill_catalog}` is replaced with the structured catalog generated from the `SkillStore`. The prompt is rebuilt on every `agent_node` call, so newly added skills appear without restarting the application.

**The graph uses a standard ReAct loop**. The model decides whether to respond directly or call a tool. If it calls a tool, execution moves to the `tool_node`, then back to the agent.

``` mo
builder = StateGraph(State, context_schema=Context)
builder.add_node(agent_node)
builder.add_node(tool_node)
builder.add_node(error_summary_node)

builder.add_edge("__start__", "agent_node")
builder.add_conditional_edges(
    "agent_node",
    should_continue,
    {"tool_node": "tool_node", "error_summary_node": "error_summary_node", "__end__": "__end__"},
)
builder.add_edge("tool_node", "agent_node")
builder.add_edge("error_summary_node", "__end__")

graph = builder.compile(name="SkillsAgent")
```

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm td">
<img src="https://miro.medium.com/v2/resize:fit:700/1*h0HuEy2SSYZDrlVym7EdOQ.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="165" />
</div>
</div>
</figure>

There is no separate planning phase for skills. The decision to load a skill is made inside the same loop that handles tool calls. The g**raph enforces retry limits and error handling**, but skill selection itself is driven by the prompt rather than by explicit routing logic.

### Runtime Configuration with Context

The `Context` dataclass acts as both configuration input and a container for runtime resources used by the graph.

``` mo
@dataclass(kw_only=True)
class Context:
    # LLM Configuration
    provider: str = "ollama"       # "ollama" or "openai"
    ollama_model: str = "qwen3"
    openai_model: str = "gpt-5-mini"
    base_url: str = "http://localhost:11434"

    # Skills Configuration
    skills_dir: str = field(default_factory=_get_default_skills_dir)
```

Defaults can be overridden through environment variables. The priority order is:

1.  <span id="f7fe">explicit constructor arguments</span>
2.  <span id="3027">environment variables</span>
3.  <span id="5e70">hardcoded defaults</span>

This allows the same graph definition to run locally with defaults and in containerized environments with environment based configuration.

> I found that **gpt-5-mini** worked best with this setup. In my tests, **gpt-4o-mini** was less consistent in triggering the expected tool calls for this workflow.
>
> If you are using Ollama, I recommend choosing a **stronger model**. Alternatively, you may need to **adjust the system prompt** or introduce additional **enforcement logic** in the graph to ensure tool calls are triggered when required.

</div>

</div>

</div>

<div class="v cf qt qu qv qw" role="separator">

<span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz"></span>

</div>

<div class="gc gd ge gf gg">

<div class="v cf">

<div class="cm bd fo fp fq fr">

## Design Tradeoffs and Failure Modes

### The multi phase pipeline

I’ve built the first version separating skill selection, loading, and execution into different nodes:

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm te">
<img src="https://miro.medium.com/v2/resize:fit:700/1*IJWR9pB-MOu5ei1Fb-YJbA.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="53" />
</div>
</div>
</figure>

The state included fields such as `selected_skill`, `skill_context`, and `execution_phase`. On paper, the separation looked clean.

In practice, it introduced **complexity**. The planning node could select one skill, while the execution step discovered it needed another. Conditional routing became harder to maintain. Adding a new skill required updating graph logic rather than just adding a file.

The LLM was already capable of chaining tool calls inside a single ReAct loop. After simplifying the graph, the state was reduced to `tool_retry_attempts` and `tool_call_count`. Skill ordering moved from explicit routing to instructions in the system prompt.

<div class="tf v">

<div class="e">

</div>

</div>

The graph now enforces hard constraints such as retry limits and error handling. Skill selection itself is handled through prompting rather than topology.

### Tool call and ToolMessage invariants

The OpenAI Chat Completions API requires that every `tool_call` in an `AIMessage` has a corresponding `ToolMessage` with the same `tool_call_id`.

If a tool crashes before producing a `ToolMessage`, the checkpointer can persist an invalid state. On the next request, the API rejects the conversation because the invariant is broken.

To prevent this, the tool node includes a fallback that always generates `ToolMessage` responses when an exception occurs:

``` mo
def _create_fallback_error_messages(tool_calls, error_message) -> list[ToolMessage]:
    """Last-resort guarantee that the tool_call/ToolMessage invariant holds."""
    return [
        ToolMessage(
            content=f"TOOL_ERROR type=internal retryable=false\nmessage: {error_message}",
            tool_call_id=_extract_tool_call_id(tc),
        )
        for tc in tool_calls
    ]
```

This guarantees that state remains valid even if tool execution fails unexpectedly.

### Retry detection via exception parsing

Some tool adapters do not expose structured status codes. Errors are returned as generic exceptions with status information embedded in the message string.

To distinguish *retryable* errors from permanent ones, the implementation parses the exception message:

``` mo
def _is_retryable_error(exc: BaseException) -> bool:
    if isinstance(exc, ExceptionGroup):
        return any(_is_retryable_error(sub) for sub in exc.exceptions)
    msg = str(exc)
    return any(code in msg for code in RETRYABLE_STATUS_CODES)
```

> *The* *`langchain-mcp-adapters`* *library, for example, doesn’t expose structured HTTP status codes. Errors come back as generic* *`Exception`* *objects with the status code somewhere in the message string. I was using this library on a project with agent skills and MCP.*

`ExceptionGroup` handling is necessary when multiple async tool calls fail concurrently in Python 3.11+.

This approach depends on message formatting and may break if libraries change their error strings. It is not ideal, but it is an option when structured error types are unavailable.

### The Error Summary Node

When the retry limit is reached, the agent must stop. Returning a raw exception is not useful, and allowing the model to keep its tools would likely trigger more retries.

Instead, the graph calls the model one final time without binding any tools. The model receives the conversation state and produces a plain text summary.

``` mo
async def error_summary_node(state: State, runtime: Runtime[Context]) -> dict:
    ctx = runtime.context
    model = load_chat_model(ctx.provider, ctx.model, ctx.base_url)

    response = await model.ainvoke([
        {"role": "system", "content": ERROR_SUMMARY_SYSTEM_PROMPT},
        *state["messages"],
    ])

    return {"messages": [response], "tool_retry_attempts": 0}
```

Because no tools are bound in this node, the model can only produce text. This ensures that **execution ends with a human readable explanation** rather than another tool call.

This is one place where graph structure enforces behavior. When retry limits are exceeded, the flow is forced to terminate through this node.

### The Agent and Tool Nodes

The agent node is responsible for reasoning and deciding whether to call tools.

On each iteration, it builds the system prompt using the current skill catalog, binds available tools, and invokes the model.

``` mo
async def agent_node(state: State, runtime: Runtime[Context]) -> dict:
    ctx = runtime.context

    if ctx._skill_store is None:
        await ctx.initialize()

    tools: list[BaseTool] = ctx.tools

    system_message = SYSTEM_PROMPT.format(
        current_time=datetime.now(tz=UTC).isoformat(),
        skill_catalog=ctx.skill_store.get_skill_catalog(),
    )

    model = load_chat_model(
        provider=ctx.provider,
        model=ctx.model,
        base_url=ctx.base_url,
    )
    model_with_tools = model.bind_tools(tools) if tools else model

    response = await model_with_tools.ainvoke(
        [{"role": "system", "content": system_message}, *state["messages"]]
    )
    return {"messages": [response]}
```

The `context_schema=Context` parameter allows LangGraph to inject configuration and runtime objects into each node through `runtime.context`, avoiding global state.

The tool node executes tool calls and handles retries.

``` mo
async def _invoke_with_retry(tool: BaseTool, args: dict) -> Any:
    for attempt in range(MAX_INVOKE_RETRIES + 1):
        try:
            return await tool.ainvoke(args)
        except Exception as e:
            if attempt < MAX_INVOKE_RETRIES and _is_retryable_error(e):
                delay = 2**attempt
                await asyncio.sleep(delay)
            else:
                raise
```

Errors are returned to the model in a structured format:

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm tg">
<img src="https://miro.medium.com/v2/resize:fit:700/1*9ubYYSsMUfzK2p4Yf7rniQ.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="108" />
</div>
</div>
</figure>

After each agent turn, routing logic checks two limits:

``` mo
def should_continue(state: State) -> Literal["tool_node", "error_summary_node", "__end__"]:
    last_message = state["messages"][-1]
    if isinstance(last_message, AIMessage) and last_message.tool_calls:
        if state.get("tool_call_count", 0) >= MAX_TOOL_CALLS:
            return "error_summary_node"
        if state.get("tool_retry_attempts", 0) >= MAX_TOOL_RETRIES:
            return "error_summary_node"
        return "tool_node"
    return "__end__"
```

`tool_call_count` limits total tool invocations to prevent unbounded loops.\
`tool_retry_attempts` limits repeated failures of the same operation.

These constraints are enforced at the graph level rather than through prompting.

</div>

</div>

</div>

<div class="v cf qt qu qv qw" role="separator">

<span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz"></span>

</div>

<div class="gc gd ge gf gg">

<div class="v cf">

<div class="cm bd fo fp fq fr">

## Observability

Because skills are loaded through tool calls, each `load_skill` invocation appears as a distinct event in traces. The implementation also emits structured logs for each tier:

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm th">
<img src="https://miro.medium.com/v2/resize:fit:700/1*0LGAzkLn39NbvPNgP6q64A.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="67" />
</div>
</div>
</figure>

Each entry records the tier, operation, duration, and a short summary. With tracing tools such as Langfuse, this makes it possible to inspect:

- <span id="ae37">Which skills were loaded for a given task;</span>
- <span id="609f">Whether some skills are never used;</span>
- <span id="32b2">How much additional context is loaded per conversation;</span>
- <span id="a259">Which tool calls fail frequently.</span>

Since skill loading is explicit, it can be analyzed alongside downstream tool usage and final responses.

### A Typical Interaction

Example request:

> *Help me understand common n8n workflow patterns for API integrations*

**Turn 1:** The model reviews the skill catalog and calls:

``` mo
load_skill("n8n-skills")
```

**Turn 2:** After receiving the main instructions, it requests a specific reference file:

``` mo
read_skill_file("n8n-skills", "resources/guides/usage-guide.md")
```

**Turn 3:** With the relevant content loaded, it produces a response using that context.

The sequencing is handled within the same ReAct loop used for other tools. No additional orchestration layer is required.

### Adding a New Skill

Adding a skill does not require changes to the graph or tool definitions.

Create a new directory under `skills/`:

``` mo
skills/
└── code-review/
    ├── SKILL.md
    ├── checklist.md
    └── examples.md
```

Define metadata and instructions in `SKILL.md`:

``` mo
---
name: code-review
description: "Code review guidelines and checklist."
tags:
  - engineering
  - quality
---
```

Restart the agent, or invalidate the store cache during development. On the next scan, the skill appears in the catalog and becomes available through `load_skill`.

Because skills are regular files in a repository, they can be **versioned** and reviewed like any other documentation. Updating a skill does not require modifying the Python code.

## Deployment and Runtime Environment

The agent runs on Aegra, an open source LangGraph platform. It manages routing, PostgreSQL state persistence, authentication, and streaming.

<div class="pl pm pn po pp pq">

<a href="https://github.com/ibbybuilds/aegra?source=post_page-----a9856378e8f6---------------------------------------" rel="noopener  ugc nofollow" target="_blank"></a>

<div class="pr v bx">

<div class="ps v cs cf ca pt">

## GitHub - ibbybuilds/aegra: Open source LangGraph Platform alternative - Self-hosted AI agent…

<div class="qb e">

### Open source LangGraph Platform alternative - Self-hosted AI agent backend with FastAPI and PostgreSQL. Zero vendor…

</div>

<div class="qc e">

github.com

</div>

</div>

<div class="qd e">

<div class="ti e qf qg qh qd qi lv pq">

</div>

</div>

</div>

</div>

This keeps the skills implementation focused on domain logic rather than infrastructure concerns. At runtime, Aegra loads the graph and manages request handling and persistence:

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm tj">
<img src="https://miro.medium.com/v2/resize:fit:700/1*7bPklgJyn4twYexAzhj9JA.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="265" />
</div>
</div>
</figure>

### Running Locally

To test locally, start the server that hosts your LangGraph graph.

**GitHub repo:** <a href="https://github.com/pessini/langgraph-skills-agent" class="z oy" rel="noopener ugc nofollow" target="_blank"><strong>https://github.com/pessini/langgraph-skills-agent</strong></a>

``` mo
cp .env.example .env
```

Set `LLM_PROVIDER=openai` and configure `OPENAI_API_KEY`, then run:

``` mo
docker compose up -d
```

Docker handles database initialization, migrations, and server startup.

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm tk">
<img src="https://miro.medium.com/v2/resize:fit:700/1*MPr46eGks89ERkyxmkf-9Q.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="418" />
</div>
</div>
</figure>

``` mo
curl http://localhost:8000/health

# You should get something like:
# {"status":"healthy","database":"connected","langgraph_checkpointer":"connected","langgraph_store":"connected"}
```

### Testing the Agent

You can test the agent using the <a href="https://github.com/langchain-ai/agent-chat-ui" class="z oy" rel="noopener ugc nofollow" target="_blank">LangChain Agent Chat UI</a>:

<a href="https://agentchat.vercel.app/" class="z oy" rel="noopener ugc nofollow" target="_blank">https://agentchat.vercel.app</a>

The UI connects to any LangGraph compatible endpoint. To use it, you need:

- <span id="5ba4">The public or local URL of your deployment</span>
- <span id="21f0">The Graph ID or graph name of your running agent</span>

Fill in the connection fields in the UI and connect to your deployed graph.

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm tl">
<img src="https://miro.medium.com/v2/resize:fit:700/1*BCpGwMnJS1mssEDzOikqwA.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="648" />
</div>
</div>
</figure>

### Tier Behavior in Practice

The **Tiers of Knowledge** model becomes visible when testing different types of requests.

If you send a simple greeting such as:

*Hello*

The agent stays at Tier 1. Only the skill catalog is present. No skills are loaded.

If you ask:

*What is n8n?*

The agent loads the relevant skill using `load_skill(...)`. This moves to Tier 2, where the full `SKILL.md` instructions are available.

If you ask something more specific, such as:

*I want to automate multi platform social media content creation with AI. How can I do it?*

The agent may load the main skill first, then request additional reference files using `read_skill_file(...)`. This activates Tier 3, where detailed supporting documentation is pulled in.

<figure class="mo mp mq mr ms mt ml mm paragraph-image">
<div class="mu mv ej mw bd mx" role="button" tabindex="0">
<span class="em eo ep ai eq er es et eu speechify-ignore">Press enter or click to view image in full size</span>
<div class="ml mm tm">
<img src="https://miro.medium.com/v2/resize:fit:700/1*_NydJ1OOfTalfBGj-9e3eQ.png" class="bd lv my mz" loading="lazy" role="presentation" width="700" height="94" />
</div>
</div>
</figure>

For this question, the reference **file=resources/templates/ai-chatbots/3066-automate-multi-platform-social-media-content-creation-with-a.md** was loaded.

By varying the complexity of the prompt, you can observe how much knowledge is loaded and how the system scales its context accordingly.

</div>

</div>

</div>

<div class="v cf qt qu qv qw" role="separator">

<span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz ra"></span><span class="qx bu bi qy qz"></span>

</div>

<div class="gc gd ge gf gg">

<div class="v cf">

<div class="cm bd fo fp fq fr">

## Closing Thoughts and Next Steps

The implementation is compact, under 600 lines of Python, most of it error handling.

If I were starting over, here is what I would keep in mind:

### Start with one ReAct loop

Do not build a multi phase pipeline. The LLM can chain tool calls on its own. Use the graph for hard constraints such as retry limits and termination rules. Use the prompt for behavioral guidance such as loading skills first.

### Skills are just markdown

Keep the format simple. YAML frontmatter for metadata, markdown for instructions, one directory per skill. The quality of the content matters more than the structure.

### The system prompt does most of the work

The task to skill mapping matters more than additional graph nodes. If the model cannot reliably decide which skill to load, the rest of the architecture does not compensate for it.

### **Watch the edge cases**

Validate file paths. Preserve the tool_call and ToolMessage invariant. Handle ExceptionGroup when running async tools. Be aware of MCP session pool limits. These issues tend to appear only under real load.

### Inspect what the agent loads

Since skills are loaded through tool calls, you can trace them. Reviewing traces helps identify unused skills, repeated failures, and unnecessary context loading.

> This pattern is not tied to a specific model or domain. The only requirement is that someone writes down what "doing it right" looks like for their domain.

This also raises the possibility of using **Langfuse Prompt Management** to store and load skills instead of relying only on local markdown files. It would allow a direct comparison of operational tradeoffs.

**Potential advantages** include centralized skill management, built in versioning and rollback, and easier experimentation across skill revisions.

The **tradeoffs** are additional external dependencies, possible latency overhead, and more complexity in local development.

This would not change the architecture, but it may simplify operations and experimentation.

**GitHub repo:** <a href="https://github.com/pessini/langgraph-skills-agent" class="z oy" rel="noopener ugc nofollow" target="_blank"><strong>https://github.com/pessini/langgraph-skills-agent</strong></a>

## References

LangGraph by LangChain\
<a href="https://github.com/langchain-ai/langgraph" class="z oy" rel="noopener ugc nofollow" target="_blank">https://github.com/langchain-ai/langgraph</a>

Aegra — Open-source LangGraph platform by Muhammad Ibrahim\
<a href="https://github.com/ibbybuilds/aegra" class="z oy" rel="noopener ugc nofollow" target="_blank">https://github.com/ibbybuilds/aegra</a>

n8n Skills Repository by haunchen\
<a href="https://github.com/haunchen/n8n-skills/" class="z oy" rel="noopener ugc nofollow" target="_blank">https://github.com/haunchen/n8n-skills/</a>

</div>

</div>

</div>

</div>

</div>

</div>

</div>
