<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Jacob Scripting]]></title><description><![CDATA[Writing about the next stage of my journey!]]></description><link>https://blog.jacobscript.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 14 Sep 2026 23:26:48 GMT</lastBuildDate><atom:link href="https://blog.jacobscript.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Using ACP + Deep Agents to Demystify Modern Software Engineering]]></title><description><![CDATA[This blog originally appeared as a guest post on JetBrains' developer blog.
I've come to accept that I will delegate an ever-increasing amount of my work as a software engineer to LLMs. I was an early]]></description><link>https://blog.jacobscript.dev/using-acp-deep-agents-to-demystify-modern-software-engineering</link><guid isPermaLink="true">https://blog.jacobscript.dev/using-acp-deep-agents-to-demystify-modern-software-engineering</guid><category><![CDATA[claude-code]]></category><category><![CDATA[agent-client-protocol]]></category><category><![CDATA[deepagents]]></category><category><![CDATA[Jetbrains]]></category><category><![CDATA[langchain]]></category><dc:creator><![CDATA[Jacob Lee]]></dc:creator><pubDate>Wed, 08 Apr 2026 16:32:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/63e5cc76f30581f89abcd198/bccaaea1-b9d8-46c3-b2a5-934c38efa0d4.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This blog originally appeared as a guest post on</em> <a href="https://blog.jetbrains.com/ai/2026/04/using-acp-deep-agents-to-demystify-modern-software-engineering/"><em>JetBrains' developer blog</em></a><em>.</em></p>
<p>I've come to accept that I will delegate an ever-increasing amount of my work as a software engineer to LLMs. I was an early Claude Code superfan, and though my ego still tells me I can write better code situationally than Anthropic's proto-geniuses in a data center, these days I'm mostly making point edits and suggestions rather than writing modules by hand.</p>
<p>This shift has made me far more productive, but I've become increasingly uncomfortable with blindly turning over such a big part of my job to an opaque third party. While training my own model was out of the question for many obvious reasons (and <a href="https://en.wikipedia.org/wiki/Explainable_artificial_intelligence">model interpretability</a> is an unsolved problem anyway), the agent harness and UX on top of it is just software, and software IS something I understand. So when I had some free time during my paternity leave, I took a stab at building some tooling to my own specifications.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63e5cc76f30581f89abcd198/6afd187b-cafd-4d9d-8caf-ca46b4109259.gif" alt="" style="display:block;margin:0 auto" />

<p>I work at a startup called <a href="https://www.langchain.com/">LangChain</a> where we've been developing our own set of open-source agentic building blocks, and I settled on building an adapter between our <a href="https://docs.langchain.com/oss/python/deepagents/overview">Deep Agents</a> framework and <a href="https://agentclientprotocol.com/overview/introduction">Agent Client Protocol (ACP)</a>. My goal was just to build a bespoke coding agent that fit my workflows, but the results were better than I expected. Over the past few months it's completely replaced Claude Code as my daily driver, with the added benefit of <a href="https://smith.langchain.com/public/fa43a7e0-c728-4d35-ba33-d7f7d66b5d63/r">full observability</a> into my agent's actions <a href="https://smith.langchain.com">via LangSmith</a> on top. In this post I'll cover how it works and how to set it up yourself!</p>
<hr />
<h2>Why an IDE + ACP Instead of a Terminal + TUI?</h2>
<p>If you're not familiar with ACP, it's an open protocol that defines how a client (most often but not limited to IDEs like WebStorm or Zed) interacts with AI agents. It allows you to do cool things like quickly pass a coding agent the exact context you're looking at in an IDE.</p>
<p>I've gotten quite used to being productive in IDEs over my decade writing software professionally, and still find them valuable for a few reasons:</p>
<ul>
<li><p>I do still edit code by hand occasionally. Most often there are small edits I can make faster than explaining the problem to an agent, or because I can do something in parallel alongside a running agent like adding debug statements, but this still provides <em>some</em> alpha.</p>
</li>
<li><p>IDEs are fantastic interfaces for viewing code in context. I most often use this to understand the general scope of a problem before prompting, or to self-review my current branch, but it's also often just faster for me to point the agent at a file rather than asking it to <code>grep</code> around.</p>
</li>
</ul>
<p>I previously used Claude Code in a separate terminal pane in an IDE, which worked but always felt like two disconnected tools. In JetBrains IDEs, the agent lives in a native tool window with tight integration. I can <code>@mention</code> the file or block of code I'm currently looking at, and many of my threads are littered with messages like "Take a look at this. Does it look funny? <code>@thisFile</code>".</p>
<hr />
<h2>How it Works</h2>
<h3>The Agent</h3>
<p>Though I could have created the various pieces for my agent from scratch, Deep Agents provided a good, opinionated starting point providing the following:</p>
<ul>
<li><p>Tools around interacting with the filesystem (<code>read/write/edit_file</code>, <code>ls</code>, <code>grep</code>, etc.)</p>
</li>
<li><p><a href="https://docs.langchain.com/oss/python/deepagents/backends"><strong>Shell access</strong></a>, which allows the agent to run verifications like lint, tests, and more</p>
<ul>
<li>Alongside this, <a href="https://docs.langchain.com/oss/python/deepagents/human-in-the-loop"><strong>human-in-the-loop</strong></a> support to allow restricting dangerous actions</li>
</ul>
</li>
<li><p>A <code>write_todos</code> tool, which encourages the agent to take a <strong>planning step</strong> that breaks work into steps and tracks progress</p>
<ul>
<li>In practice this makes a big difference on longer refactors to keep the agent focused</li>
</ul>
</li>
<li><p>Capabilities around <a href="https://docs.langchain.com/oss/python/deepagents/subagents"><strong>spawning isolated sub-agents</strong></a> for parallel or compartmentalized work</p>
<ul>
<li>Each one gets its own context, runs independently, and reports back, keeping the model's context window manageable</li>
</ul>
</li>
<li><p>Other important UX features like <a href="https://docs.langchain.com/oss/python/deepagents/streaming"><strong>streaming</strong></a>, cancellation, prompt caching, context summarization</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/63e5cc76f30581f89abcd198/5f0ff662-bc2b-4f8d-8448-d9a17056e86c.png" alt="" style="display:block;margin:0 auto" />

<p>I also added some custom middleware that appends information about the current project setup in the system prompt, such as the current directory open in the IDE, whether a <code>git</code> repo was present, package manager detection, and more.</p>
<p>It's also possible to add skills, tweak the system prompt, add custom tools or MCPs, and more directly in Python rather than as a config option of a CLI.</p>
<h3>The ACP Adapter</h3>
<p>After deciding on a basic agent setup, I needed to hook that agent into the client via ACP. I created an adapter that implements the ACP protocol interface and handles session lifecycle, message routing, model switching, and streaming.</p>
<p>One nice surprise was how cleanly the agent’s capabilities mapped onto ACP concepts.</p>
<p>For example:</p>
<ul>
<li><p>The agent’s planning step (<code>write_todos</code>) maps naturally to <a href="https://agentclientprotocol.com/protocol/agent-plan">agent plans</a> in ACP</p>
</li>
<li><p>Interrupts from the agent (e.g. “I want to run this command”) map to <a href="https://agentclientprotocol.com/protocol/tool-calls#requesting-permission"><strong>permission requests</strong></a></p>
</li>
<li><p>Threads and session persistence were nearly 1:1 with Deep Agents checkpointers</p>
</li>
</ul>
<p>This meant I didn’t need to invent much glue logic - the protocol already had good primitives for most of what I wanted. The overall agent runner looks roughly like this, minus tool call and message formatting:</p>
<pre><code class="language-python">current_state = None
user_decisions = []
while current_state is None or current_state.interrupts:
    # Check for cancellation
    if self._cancelled:
        self._cancelled = False  # Reset for next prompt
        return PromptResponse(stop_reason="cancelled")

    async for stream_chunk in agent.astream(
        Command(resume={"decisions": user_decisions})
        if user_decisions
        else {"messages": [{"role": "user", "content": content_blocks}]},
        config=config,
        stream_mode=["messages", "updates"],
        subgraphs=True,
    ):
        if stream_chunk.__interrupt__:
            # If Deep Agents interrupts, request next actions from
            # the client via ACP's session/request_permission method
            user_decisions = await self._handle_interrupts(
                current_state=current_state,
                session_id=session_id,
            )
            # Break out of the current Deep Agent stream. The while
            # loop above resumes it with the user decisions
            # returned from the session/request_permission method
            break

        # ...translate LangGraph output into ACP
        # Tools that do not require interrupts are called
        # internally results are just streamed back here as well

        # current_state will be none when the agent has finished
        current_state = await agent.aget_state(config)

return PromptResponse(stop_reason="end_turn")
</code></pre>
<p>The human-in-the-loop flow was where I spent the most time. When the agent wants to run a shell command or make a file edit that requires approval, the adapter intercepts the interrupt from Deep Agents, and depending on what permissions mode the user has selected and what they have previously approved, either resumes immediately or sends a permission request to the IDE with options to <code>approve</code>, <code>reject</code>, or <code>always-allow</code> that command type.</p>
<p>The <code>always-allow</code> is session-scoped - if you approve <code>uv sync</code> once and choose "always allow", subsequent <code>uv sync</code> calls skip the prompt automatically, but I made efforts to prevent similar commands such as <code>uv run script.py</code> from bypassing the permission check.</p>
<p>Here's how the end result looks in WebStorm:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63e5cc76f30581f89abcd198/be3d705a-4705-4ff8-a6d2-822dc0810d15.png" alt="" style="display:block;margin:0 auto" />

<h2>How it Went</h2>
<p>While I haven't run formal evals, I was pleasantly surprised by how well my agent performed after only a few iterations. I didn't actually expect to switch off of Claude Code, and it was a great dogfooding exercise as well, since our OSS team was able to upstream some of my feedback back into Deep Agents itself.</p>
<p>My original goal of regaining code-level, rather than config-level, control over my daily workflows has also been great. When Anthropic had an outage a few weeks ago, I was able to switch over to OpenAI’s <code>gpt-5.4</code> without skipping a beat, and I even found that it had some interesting quirks. I switch back and forth between models mid-session to gain different perspectives from each model when working on tricky tasks, and have also found open-source models like GLM-5 are quite capable while offering significant cost savings.</p>
<p>Another boon is observability via <a href="https://smith.langchain.com/">LangSmith tracing</a>, which allows me to debug and improve my agent when I run into issues. Being able to see exactly what context was passed to the model, which tools it called, and where it went sideways helped me understand behaviors that were previously hidden inside the harness. <a href="https://smith.langchain.com/public/fa43a7e0-c728-4d35-ba33-d7f7d66b5d63/r">Here's an example</a> of what such a trace looks like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63e5cc76f30581f89abcd198/7ba784b0-fc75-433d-ba61-6b3159c314c0.png" alt="" style="display:block;margin:0 auto" />

<p>For example, when I noticed that my agent was starting to take wide, slow sweeps of my filesystem, I used a trace to find a bug in my system prompt that told the agent the project was at the filesystem root rather than the current working directory.</p>
<h2>Taking Back Your Dev Workflows for Fun and Profit</h2>
<p>What started as a small late-night project I worked on around taking care of a newborn daughter turned into a huge success both for my own understanding of agent behavior and for improving my daily workflow.</p>
<p>It proved to me that Claude Code isn't magic but a bundle of very clever tricks rolled up into a neat package. The harness layer is just software, and software is something any developer can shape to fit how they want to work.</p>
<p>If you’re curious, I’d highly recommend trying an experiment like this yourself. Even a small prototype can teach you a lot about how these systems think and where they break. Clone the repo and <a href="https://github.com/langchain-ai/deepagents/blob/main/libs/acp/README.md">follow the setup guide here</a> to get started from source code. I'd love to know what you think - you can reach out to me on X <a href="https://x.com/Hacubu">@Hacubu</a>!</p>
<p>Special thanks to <a href="https://x.com/veryboldbagel">@veryboldbagel</a> and <a href="https://x.com/masondrxy">@masondrxy</a> for helping productionize the adapter and dealing with my unending questions and feedback!</p>
]]></content:encoded></item><item><title><![CDATA[How to Set Up a Private OpenAI-Compatible LLM on Google Cloud Run]]></title><description><![CDATA[For those passionate about privacy and control, the trajectory of improvement in open-weight LLMs and the ecosystem around them has been extremely encouraging:

The gap in raw reasoning capability between the best OSS models (DeepSeek, Qwen) and the ...]]></description><link>https://blog.jacobscript.dev/private-llm-google-cloud</link><guid isPermaLink="true">https://blog.jacobscript.dev/private-llm-google-cloud</guid><category><![CDATA[llm]]></category><category><![CDATA[APIs]]></category><category><![CDATA[google cloud]]></category><category><![CDATA[GPU]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Jacob Lee]]></dc:creator><pubDate>Thu, 05 Jun 2025 16:20:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749149192554/aa7f2bc6-dd4f-43be-bf87-82de8b7d9aac.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For those passionate about privacy and control, the trajectory of improvement in open-weight LLMs and the ecosystem around them has been extremely encouraging:</p>
<ul>
<li><p>The gap in raw reasoning capability between the best OSS models (DeepSeek, Qwen) and the best models from frontier labs has continued to shrink.</p>
</li>
<li><p>OSS models now support key usability features such as function calling and structured output.</p>
</li>
<li><p>Thanks to tools such as <a target="_blank" href="https://ollama.ai/">Ollama</a> and the proliferation of GPUs in consumer laptops, more people than ever can discover and run models on their own hardware.</p>
</li>
</ul>
<p>This continued trend means that even distilled versions of the best OSS models will become “good enough” for an increasing percentage of tasks. However, when it comes time to do something like serve an app to actual users, there’s a missing piece in this AI-hacker fairytale - <strong>how do I easily deploy a model on infrastructure I control</strong>?</p>
<p>When I saw that Google recently <a target="_blank" href="https://cloud.google.com/blog/products/serverless/cloud-run-gpus-are-now-generally-available">brought serverless GPUs into GA</a>, it piqued my interest, and I was able to adapt one of their examples to support API key auth and make it fully OpenAI and LangChain-compatible. I wrote up a small guide here + <a target="_blank" href="https://github.com/jacoblee93/personallm">a GitHub repo</a> you can use to deploy your own!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749149071019/684386a2-eae6-4d7f-8040-827394f5d10e.jpeg" alt class="image--center mx-auto" /></p>
<p>Once live, your endpoint can be used as a drop-in substitute for clients and code that use these interfaces. It also requires no infrastructure management and scales down to zero instances when not in use.</p>
<p>You can serve any open source model from <a target="_blank" href="https://ollama.com/search">Ollama's registry</a> in theory, including <a target="_blank" href="https://ollama.com/library/deepseek-r1:14b">DeepSeek</a>, <a target="_blank" href="https://ollama.com/library/gemma3:4b">Gemma</a>, and <a target="_blank" href="https://ollama.com/library/qwen3">Qwen</a>, though in practice caps on Cloud Run resources will limit effective model size. For more on this, see the below section on model customization.</p>
<p>Let’s dive in!</p>
<h2 id="heading-quickstart">Quickstart</h2>
<h3 id="heading-setting-up-google-cloud-resources">Setting up Google Cloud resources</h3>
<blockquote>
<p>The initial setup for this project is the same as the official Cloud Run guide <a target="_blank" href="https://cloud.google.com/run/docs/tutorials/gpu-gemma-with-ollama">here</a>.</p>
</blockquote>
<p>If you don't already have a Google Cloud account, you will first need to <a target="_blank" href="https://cloud.google.com/">sign up</a>.</p>
<p>Navigate to the <a target="_blank" href="https://console.cloud.google.com/projectselector2/home/dashboard">Google Cloud project selector</a> and select or create a Google Cloud project. You will need to <a target="_blank" href="https://cloud.google.com/billing/docs/how-to/verify-billing-enabled#confirm_billing_is_enabled_on_a_project">enabled billing for the project</a>, since GPUs are currently not part of Google Cloud's free tier.</p>
<p>Next, you must enable access to <strong>Artifact Registry</strong>, <strong>Cloud Build</strong>, <strong>Cloud Run</strong>, and <strong>Cloud Storage APIs</strong> for your project. <a target="_blank" href="https://console.cloud.google.com/apis/enableflow?apiid=artifactregistry.googleapis.com,cloudbuild.googleapis.com,run.googleapis.com,storage.googleapis.com">Click here</a>, select your newly created project, then follow the instructions to do so.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749056695474/4a827e90-fb01-4b7a-a9f8-66f83da864c4.png" alt class="image--center mx-auto" /></p>
<p>GPUs are not part of the default project quota, so you will need to submit a quota increase request. From <a target="_blank" href="https://console.cloud.google.com/projectselector2/iam-admin/quotas">this page</a>, select your project, then filter by <code>Total Nvidia L4 GPU allocation without zonal redundancy, per project per region</code> in the search bar. Find your desired region (Google currently recommends <code>europe-west1</code>, note that <a target="_blank" href="https://cloud.google.com/run/pricing">pricing</a> may vary depending on region), then click the side menu and press <code>Edit quota</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749056716918/e5f77afe-0825-4113-b1f4-bf8e10033c17.png" alt class="image--center mx-auto" /></p>
<p>Enter a value (e.g. <code>5</code>), and submit a request. Google claims that increase requests may take a few days to process, but you may receive an approval email almost immediately in practice.</p>
<p>Finally, you will need to set up proper IAM permissions for your project. Navigate to <a target="_blank" href="https://console.cloud.google.com/projectselector2/iam-admin/iam">this page</a> and select your project, then press <code>Grant Access</code>. In the resulting modal, paste the following permissions into the filter window and add them one by one to a principal on your project:</p>
<ul>
<li><p><code>roles/artifactregistry.admin</code></p>
</li>
<li><p><code>roles/cloudbuild.builds.editor</code></p>
</li>
<li><p><code>roles/run.admin</code></p>
</li>
<li><p><code>roles/resourcemanager.projectIamAdmin</code></p>
</li>
<li><p><code>roles/iam.serviceAccountUser</code></p>
</li>
<li><p><code>roles/serviceusage.serviceUsageConsumer</code></p>
</li>
<li><p><code>roles/storage.admin</code></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749056734052/e6855c89-5c6d-4ac0-a9f8-82836a05e401.png" alt class="image--center mx-auto" /></p>
<p>By the end, your screen should look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749056774649/f049ca22-ba8f-49aa-ac7a-821e9a2b610d.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-deploying-your-endpoint">Deploying your endpoint</h3>
<p>Now, clone <a target="_blank" href="https://github.com/jacoblee93/personallm">this repo</a> and switch your working directory to be the cloned folder:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/jacoblee93/personallm.git
<span class="hljs-built_in">cd</span> personallm
</code></pre>
<p>The repo extends Google’s official guide a lightweight proxy server, which runs in the Cloud Run instance. This proxy handles auth and forwards requests to a concurrently running <a target="_blank" href="https://ollama.ai/">Ollama</a> instance.</p>
<p>Rename the <code>.env.example</code> file to <code>.env</code>. Run something similar to the following command to randomly generate an API key:</p>
<pre><code class="lang-bash">openssl rand -base64 32
</code></pre>
<p>Paste this value into the <code>API_KEYS</code> field. You can provide multiple API keys by comma separating them here, so make sure that none of your key values contain commas.</p>
<p>Install and initialize the <code>gcloud</code> CLI if you haven't already by <a target="_blank" href="https://cloud.google.com/sdk/docs/install">following these instructions</a>. If you already have the CLI installed, you may need to run <code>gcloud components update</code> to make sure you are on the latest CLI version.</p>
<p>Next, set your <code>gcloud</code> CLI project to be your project name:</p>
<pre><code class="lang-bash">gcloud config <span class="hljs-built_in">set</span> project YOUR_PROJECT_NAME
</code></pre>
<p>And set the region to be the same one as where you requested GPU quota:</p>
<pre><code class="lang-bash">gcloud config <span class="hljs-built_in">set</span> run/region YOUR_REGION
</code></pre>
<p>Finally, run the following command to deploy your new inference endpoint!</p>
<pre><code class="lang-bash">gcloud run deploy personallm \
  --<span class="hljs-built_in">source</span> . \
  --concurrency 4 \
  --cpu 8 \
  --set-env-vars OLLAMA_NUM_PARALLEL=4 \
  --gpu 1 \
  --gpu-type nvidia-l4 \
  --max-instances 1 \
  --memory 32Gi \
  --no-cpu-throttling \
  --no-gpu-zonal-redundancy \
  --timeout=600
</code></pre>
<p>When prompted with something like <code>Allow unauthenticated invocations to [personallm] (y/N)?</code>, you should respond with <code>y</code>. The internal proxy will handle authentication, and we want our endpoint to be reachable from anywhere for ease of use.</p>
<p>Note that deployments are quite slow since model weights are bundled directly into the Dockerfile - expect this step to take upwards of 20 minutes. Once it finishes, your terminal should print a <code>Service URL</code>, and that's it! You now have a personal, private LLM inference endpoint!</p>
<h2 id="heading-trying-it-out">Trying it out</h2>
<p>You can call your endpoint in a similar way to how you'd call an OpenAI model, only using your generated API key and your provisioned endpoint. Here are some examples:</p>
<h3 id="heading-openai-python-sdk">OpenAI Python SDK</h3>
<pre><code class="lang-bash">uv add openai
</code></pre>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> openai <span class="hljs-keyword">import</span> OpenAI

<span class="hljs-comment"># Note the /v1 suffix</span>
client = OpenAI(
    base_url=<span class="hljs-string">"https://YOUR_SERVICE_URL/v1"</span>,
    api_key=<span class="hljs-string">"YOUR_API_KEY"</span>,
)

response = client.chat.completions.create(
    model=<span class="hljs-string">"qwen3:14b"</span>,
    messages=[
      {<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: <span class="hljs-string">"What is 2 + 2?"</span>}
    ]
)
</code></pre>
<p>See <a target="_blank" href="https://platform.openai.com/docs/overview">OpenAI's SDK docs</a> for examples of advanced features such as <a target="_blank" href="https://platform.openai.com/docs/guides/function-calling?api-mode=chat">function/tool calling</a>.</p>
<h3 id="heading-langchain">LangChain</h3>
<pre><code class="lang-bash">uv add langchain-ollama
</code></pre>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> langchain_ollama <span class="hljs-keyword">import</span> ChatOllama

model = ChatOllama(
    model=<span class="hljs-string">"qwen3:14b"</span>,
    base_url=<span class="hljs-string">"https://YOUR_SERVICE_URL"</span>,
    client_kwargs={
      <span class="hljs-string">"headers"</span>: {
        <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">"Bearer YOUR_API_KEY"</span>
      }
    }
)

response = model.invoke(<span class="hljs-string">"What is 2 + 2?"</span>)
</code></pre>
<p>See <a target="_blank" href="https://python.langchain.com/">LangChain's docs</a> for examples of advanced features such as <a target="_blank" href="https://python.langchain.com/docs/how_to/tool_calling/">function/tool calling</a>.</p>
<h3 id="heading-openai-js-sdk">OpenAI JS SDK</h3>
<pre><code class="lang-bash">npm install openai
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> OpenAI <span class="hljs-keyword">from</span> <span class="hljs-string">"openai"</span>;

<span class="hljs-comment">// Note the /v1 suffix</span>
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> OpenAI({
  baseURL: <span class="hljs-string">"https://YOUR_SERVICE_URL/v1"</span>,
  apiKey: <span class="hljs-string">"YOUR_API_KEY"</span>,
});

<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> client.chat.completions.create({
  model: <span class="hljs-string">"qwen3:14b"</span>,
  messages: [{ role: <span class="hljs-string">"user"</span>, content: <span class="hljs-string">"What is 2 + 2?"</span> }],
});
</code></pre>
<p>See <a target="_blank" href="https://platform.openai.com/docs/overview">OpenAI's SDK docs</a> for examples of advanced features such as <a target="_blank" href="https://platform.openai.com/docs/guides/function-calling?api-mode=chat">function/tool calling</a>.</p>
<h3 id="heading-langchainjs">LangChain.js</h3>
<pre><code class="lang-bash">npm install @langchain/ollama @langchain/core
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ChatOllama } <span class="hljs-keyword">from</span> <span class="hljs-string">"@langchain/ollama"</span>;

<span class="hljs-keyword">const</span> model = <span class="hljs-keyword">new</span> ChatOllama({
  model: <span class="hljs-string">"qwen3:14b"</span>,
  baseUrl: <span class="hljs-string">"https://YOUR_SERVICE_URL"</span>,
  headers: {
    Authorization: <span class="hljs-string">"Bearer YOUR_API_KEY"</span>,
  },
});
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> model.invoke(<span class="hljs-string">"What is 2 + 2?"</span>);
</code></pre>
<p>See <a target="_blank" href="https://js.langchain.com/">LangChain's docs</a> for examples of advanced features such as <a target="_blank" href="https://js.langchain.com/docs/how_to/tool_calling/">function/tool calling</a>.</p>
<h3 id="heading-latency">Latency</h3>
<p>Keep in mind that there will be additional cold start latency if the endpoint has not been used in some time.</p>
<h2 id="heading-model-customization">Model customization</h2>
<p>The base configuration in this repo serves a 14 billion parameter model (<a target="_blank" href="https://ollama.com/library/qwen3:14b">Qwen 3</a>) clocked at ~20-25 output tokens per second. This model is quite capable and also supports <a target="_blank" href="https://ollama.com/blog/tool-support">function/tool calling</a>, which makes it more useful when building agentic flows, but if speed becomes a concern you might try smaller models such as Google's 4 billion parameter <a target="_blank" href="https://ollama.com/library/gemma3">Gemma 3</a>. You can also run the popular <a target="_blank" href="https://ollama.com/library/deepseek-r1:14b">DeepSeek-R1</a> if you do not need tool calling.</p>
<p>To customize the served model, open your <code>Dockerfile</code> and modify the <code>ENV MODEL qwen3:14b</code> line to be a different model from <a target="_blank" href="https://ollama.com/search">Ollama's registry</a>:</p>
<pre><code class="lang-ini"><span class="hljs-comment"># Store the model weights in the container image</span>
<span class="hljs-comment"># ENV MODEL gemma3:4b</span>
<span class="hljs-comment"># ENV MODEL deepseek-r1:14b</span>
ENV MODEL qwen3:14b
</code></pre>
<p>Note that you will also have to change your clientside code to specify the new model as a parameter.</p>
<h2 id="heading-thank-you">🙏 Thank you!</h2>
<p><a target="_blank" href="https://github.com/jacoblee93/personallm/">This GitHub repo</a> contains the source code for this guide.</p>
<p>If you have any questions or comments, please open an issue there. You can also follow me <a target="_blank" href="https://x.com/Hacubu">@Hacubu</a> on X (formerly Twitter).</p>
]]></content:encoded></item><item><title><![CDATA[Going Beyond Chatbots: How to Make GPT-4 Output Structured Data Using LangChain]]></title><description><![CDATA[This post was also featured on LangChain's official blog!
Over the past few months, I had the opportunity to do some cool exploratory work for a client that integrated LLMs like GPT-4 and Claude into their internal workflow, rather than exposing them...]]></description><link>https://blog.jacobscript.dev/going-beyond-chatbots-how-to-make-gpt-4-output-structured-data-using-langchain</link><guid isPermaLink="true">https://blog.jacobscript.dev/going-beyond-chatbots-how-to-make-gpt-4-output-structured-data-using-langchain</guid><category><![CDATA[AI]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[langchain]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Jacob Lee]]></dc:creator><pubDate>Mon, 22 May 2023 15:13:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684723466133/8bf5858c-b18b-43be-be15-1608b464f8cb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This post was also featured on</em> <a target="_blank" href="https://blog.langchain.dev/going-beyond-chatbots-how-to-make-gpt-4-output-structured-data-using-langchain/"><em>LangChain's official blog</em></a><em>!</em></p>
<p>Over the past few months, I had the opportunity to do some cool exploratory work for a client that integrated LLMs like GPT-4 and Claude into their internal workflow, rather than exposing them through a chat interface. The general idea was to take some input data, analyze it using an LLM, enrich the LLM's output using existing data sources, and then sanity check it using both traditional tools and LLMs. This process could repeat several times until finally storing a final result in a database. I've been thinking of it as a pipeline that mixes LLMs with more mundane APIs where the output of one step feeds directly into the next.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684721298189/d171ad3e-19b0-498f-923c-c268d0a7c98b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-problem">The Problem</h2>
<p>While building such pipelines, I quickly realized that while natural language is an excellent interface for a chatbot, it's quite a difficult one to use with existing APIs.</p>
<p>To illustrate this, let's say you wanted to generate and store a list of countries in Airtable. Naively asking an LLM <code>Give me a list of 5 countries</code> results in a numbered list of countries:</p>
<pre><code class="lang-javascript"><span class="hljs-string">'1. United States\n'</span> +
<span class="hljs-string">'2. Canada\n'</span> +
<span class="hljs-string">'3. United Kingdom\n'</span> +
<span class="hljs-string">'4. Australia\n'</span> +
<span class="hljs-string">'5. Japan'</span>
</code></pre>
<p>There are a few problems here - while the above output happens to be a numbered list, there is no guarantee of that. Also, you would need to write some awkward custom string parsing logic to extract the data for use in the next step of the pipeline.</p>
<p>The solution is to prompt the LLM to output data in some structured format, but it's not quite that simple. For example, asking, <code>Give me a list of 5 countries, formatted as Airtable records</code> might result in something like this:</p>
<pre><code class="lang-javascript"><span class="hljs-string">'Airtable records require a unique ID and field values in a JSON format. Here is a list of 5 countries formatted as Airtable records:\n'</span> +
<span class="hljs-string">'\n'</span> +
<span class="hljs-string">'1. {\n'</span> +
<span class="hljs-string">'  "id": "rec1",\n'</span> +
<span class="hljs-string">'  "fields": {\n'</span> +
<span class="hljs-string">'    "Country": "United States",\n'</span> +
<span class="hljs-string">'    "Continent": "North America"\n'</span> +
<span class="hljs-string">'  }\n'</span> +
<span class="hljs-string">'}\n'</span> +
<span class="hljs-string">'2. {\n'</span> +
<span class="hljs-string">'  "id": "rec2",\n'</span> +
<span class="hljs-string">'  "fields": {\n'</span> +
<span class="hljs-string">'    "Country": "Canada",\n'</span> +
<span class="hljs-string">'    "Continent": "North America"\n'</span> +
<span class="hljs-string">'  }\n'</span> +
<span class="hljs-string">'}\n'</span> +
...
</code></pre>
<p>Though the LLM (in this case GPT-4) impressively knows the general schema of an Airtable record, this is even worse than the original attempt. There is conversational text at the top that must be parsed out, and the output format is still a numbered list. Additionally, the LLM has assumed the field names of your Airtable schema, which likely do not match your internal definitions.</p>
<p>I experimented with a few custom prompting strategies like <code>Output only an array of JSON objects containing X, Y, and Z</code>, but adding such language to all my prompts quickly became tedious. Furthermore, this was somewhat unreliable due to the non-deterministic nature of LLMs, particularly with long, complex prompts and higher temperatures.</p>
<h2 id="heading-the-solution">The Solution</h2>
<p>I had already been using <a target="_blank" href="https://github.com/hwchase17/langchainjs">LangChainJS</a>, an open-source framework that helps with building complex applications around LLMs, for various pieces of the project. After asking around their Discord community, I discovered an elegant, built-in solution: <a target="_blank" href="https://js.langchain.com/docs/modules/prompts/output_parsers/#output-fixing-parser">output fixing parsers</a>!</p>
<p>Output fixing parsers contain two components:</p>
<ol>
<li><p>An easy, consistent way of generating output formatting instructions (using a popular TypeScript validation framework, <a target="_blank" href="https://github.com/colinhacks/zod">Zod</a>).</p>
</li>
<li><p>An LLM-powered recovery mechanism for handling badly formatted outputs using a more focused prompt.</p>
</li>
</ol>
<p>You could use one to solve the earlier problem like this (note that you will need to run <code>yarn add langchain</code> and <code>yarn add zod</code> if they aren't already in your dependencies):</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;
<span class="hljs-keyword">import</span> { ChatOpenAI } <span class="hljs-keyword">from</span> <span class="hljs-string">"langchain/chat_models/openai"</span>;
<span class="hljs-keyword">import</span> { PromptTemplate } <span class="hljs-keyword">from</span> <span class="hljs-string">"langchain/prompts"</span>;
<span class="hljs-keyword">import</span> { LLMChain } <span class="hljs-keyword">from</span> <span class="hljs-string">"langchain/chains"</span>;
<span class="hljs-keyword">import</span> {
  StructuredOutputParser,
  OutputFixingParser
} <span class="hljs-keyword">from</span> <span class="hljs-string">"langchain/output_parsers"</span>;

<span class="hljs-keyword">const</span> outputParser = StructuredOutputParser.fromZodSchema(
  z.array(
    z.object({
      fields: z.object({
        Name: z.string().describe(<span class="hljs-string">"The name of the country"</span>),
        Capital: z.string().describe(<span class="hljs-string">"The country's capital"</span>)
      })
    })
  ).describe(<span class="hljs-string">"An array of Airtable records, each representing a country"</span>)
);

<span class="hljs-keyword">const</span> chatModel = <span class="hljs-keyword">new</span> ChatOpenAI({
  openAIApiKey: <span class="hljs-string">"YOUR_KEY_HERE"</span>, <span class="hljs-comment">// Or set process.env.OPENAI_API_KEY</span>
  modelName: <span class="hljs-string">"gpt-4"</span>, <span class="hljs-comment">// Or gpt-3.5-turbo</span>
  temperature: <span class="hljs-number">0</span> <span class="hljs-comment">// For best results with the output fixing parser</span>
});

<span class="hljs-keyword">const</span> outputFixingParser = OutputFixingParser.fromLLM(
  chatModel,
  outputParser
);

<span class="hljs-keyword">const</span> prompt = <span class="hljs-keyword">new</span> PromptTemplate({
  template: <span class="hljs-string">`Answer the user's question as best you can:\n{format_instructions}\n{query}`</span>,
  inputVariables: [<span class="hljs-string">'query'</span>],
  partialVariables: {
    format_instructions: outputFixingParser.getFormatInstructions()
  }
});

<span class="hljs-comment">// For those unfamiliar with LangChain, a class used to call LLMs</span>
<span class="hljs-keyword">const</span> answerFormattingChain = <span class="hljs-keyword">new</span> LLMChain({
  llm: chatModel,
  prompt: prompt,
  outputKey: <span class="hljs-string">"records"</span>, <span class="hljs-comment">// For readability - otherwise the chain output will default to a property named "text"</span>
  outputParser: outputFixingParser
});

<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> answerFormattingChain.call({
  query: <span class="hljs-string">"List 5 countries."</span>
});

<span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify(result.records, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));
</code></pre>
<p>Clean and readable! And here's an example of what the results look like:</p>
<pre><code class="lang-typescript">[
  {
    <span class="hljs-string">"fields"</span>: {
      <span class="hljs-string">"Name"</span>: <span class="hljs-string">"United States"</span>,
      <span class="hljs-string">"Capital"</span>: <span class="hljs-string">"Washington, D.C."</span>
    }
  },
  {
    <span class="hljs-string">"fields"</span>: {
      <span class="hljs-string">"Name"</span>: <span class="hljs-string">"Canada"</span>,
      <span class="hljs-string">"Capital"</span>: <span class="hljs-string">"Ottawa"</span>
    }
  },
  {
    <span class="hljs-string">"fields"</span>: {
      <span class="hljs-string">"Name"</span>: <span class="hljs-string">"Germany"</span>,
      <span class="hljs-string">"Capital"</span>: <span class="hljs-string">"Berlin"</span>
    }
  },
  {
    <span class="hljs-string">"fields"</span>: {
      <span class="hljs-string">"Name"</span>: <span class="hljs-string">"Japan"</span>,
      <span class="hljs-string">"Capital"</span>: <span class="hljs-string">"Tokyo"</span>
    }
  },
  {
    <span class="hljs-string">"fields"</span>: {
      <span class="hljs-string">"Name"</span>: <span class="hljs-string">"Australia"</span>,
      <span class="hljs-string">"Capital"</span>: <span class="hljs-string">"Canberra"</span>
    }
  }
]
</code></pre>
<p>Success! The result will already be typed as an array of objects, so there's no need for <code>JSON.parse()</code> calls or any further parsing.</p>
<p>Note that the output fixing parser will throw an error if, for whatever reason, it can't generate an output matching the provided Zod schema. You could even pipe it directly into an Airtable API call!</p>
<h2 id="heading-additional-tips">Additional Tips</h2>
<ul>
<li><p>Descriptions provided with <code>.describe()</code> are optional, but give the LLM helpful context when populating individual fields. The LLM will also use clues like the field name and the overall structure of the provided schema.</p>
<ul>
<li>If you're struggling to generate output in the right format, adding descriptions or tweaking the language in these descriptions can help.</li>
</ul>
</li>
<li><p>You can use different model instances in the output fixing parser and whatever chain you're using, allowing you to mix and match temperatures and even providers for best results.</p>
</li>
</ul>
<h2 id="heading-thanks-for-reading">Thanks for Reading!</h2>
<p>I hope this post helps you better use the power of LLMs in your projects!</p>
<p>I've actually enjoyed building with LLMs and specifically <a target="_blank" href="https://js.langchain.com/">LangChain</a> so much that I recently joined their team, so expect to see more related content over the coming months! And if you have any questions or have ideas for what you'd like me to write about next, please leave a comment or reach out to me on Twitter <a target="_blank" href="https://twitter.com/hacubu">@Hacubu</a>. I'll be active in the JS channels of <a target="_blank" href="https://discord.com/invite/6adMQxSpJS">LangChain's community Discord server</a> as well.</p>
<p>Happy prompting!</p>
]]></content:encoded></item><item><title><![CDATA[How to Build a Sustainable Developer Community: A 5-Phase Framework]]></title><description><![CDATA[Authors: Janeth Graziani & Jacob Lee
Building a developer community can have numerous benefits for your company, from reducing support and onboarding costs to increasing adoption and generating valuable feedback. In this post, we’ll share our approac...]]></description><link>https://blog.jacobscript.dev/how-to-build-a-sustainable-developer-community-a-5-phase-framework</link><guid isPermaLink="true">https://blog.jacobscript.dev/how-to-build-a-sustainable-developer-community-a-5-phase-framework</guid><category><![CDATA[Developer]]></category><category><![CDATA[#community-management]]></category><category><![CDATA[discord]]></category><category><![CDATA[DevRel]]></category><category><![CDATA[General Programming]]></category><dc:creator><![CDATA[Jacob Lee]]></dc:creator><pubDate>Wed, 05 Apr 2023 15:55:13 GMT</pubDate><content:encoded><![CDATA[<p>Authors: <a target="_blank" href="https://www.linkedin.com/in/janethledezma/">Janeth Graziani</a> &amp; <a target="_blank" href="https://www.linkedin.com/in/jacoblee93/">Jacob Lee</a></p>
<p>Building a developer community can have numerous benefits for your company, from reducing support and onboarding costs to increasing adoption and generating valuable feedback. In this post, we’ll share our approach to engaging and growing a developer community in a sustainable way, based on a 5-phase framework. Whether you’re starting from scratch or looking to improve an existing community, we hope our insights will empower your team and scale your community.</p>
<p>Let’s dive in!</p>
<h2 id="heading-setting-goals-and-tone">Setting Goals and Tone</h2>
<p>No matter how large or small your company is, there are many, many benefits of building a vibrant developer community: reducing support and onboarding costs for new users, increasing adoption of your platform through referrals, retaining existing users, generating valuable feedback for your high-level product roadmaps, encouraging novel use-cases of your product, and more. Figuring out what you value most will help you better measure your results and prioritize initiatives effectively.</p>
<p>Setting the tone early for your community is crucial. It can establish an identity for your community and its members, and help make process-related decisions later on. For example, if you choose to emphasize creating a welcoming environment for inexperienced community members, this will naturally lead to policies later on that emphasize low response times for support questions and other newbie-friendly processes.</p>
<h2 id="heading-phase-1-identifying-your-developer-audience">Phase 1: Identifying your Developer Audience</h2>
<p>The first phase of this process involves identifying the specific group of developers you want to target. The developer community as a whole is huge - there are currently 28.7 million developers in the world, and that number is growing every day! It is therefore essential to pinpoint a specific group to allow you to focus your efforts initially.</p>
<p>You can choose a target developer community based on many different criteria, including region, tech stack, shared interests, or language. For companies with a live product, this group often starts with a self-selected group of users, but it’s still worth taking note of underlying similarities among your most passionate existing fans to see where your community can grow into.</p>
<p>Once you have successfully identified the audience you would like to focus on, you can move on to the second phase of the strategy.</p>
<h2 id="heading-phase-2-conducting-in-depth-research">Phase 2: Conducting In-Depth Research</h2>
<p>The goal of this phase is to understand your developer audience's pain points, values, and interests, and the best way to do this is to be embedded in your target community. Find where they congregate and go in with genuine interest to understand what they value, what tools they use, and their developer pain points. Some examples of this include:</p>
<ul>
<li><p>Attending existing community events, conferences, meetups, and online webinars</p>
</li>
<li><p>Identifying and talking with respected people within the existing community</p>
</li>
<li><p>Pay particular attention to people excited about engaging with and helping others and their style! Get to know them by name</p>
</li>
<li><p>Reading online forums and spaces where the community congregates like Reddit, <a target="_blank" href="http://Dev.to">Dev.to</a>, Hackernews etc</p>
</li>
</ul>
<h2 id="heading-phase-3-crafting-a-strategy">Phase 3: Crafting a Strategy</h2>
<p>Once you have an in-depth understanding of your target developer audience you can use this expertise to help your team craft a strategy to engage and help the developers within that audience. Use what you learned about your audience's values, interests and pain points, and bear in mind your initial goals and ways that you can guide the community towards them. Some examples could include:</p>
<ul>
<li><p>Planning a content calendar covering topics that address the identified interests, pain points, and values</p>
</li>
<li><p>Planning meetups and events that will pique their interest</p>
</li>
<li><p>Planning workshops that teach a valuable skill or tool that they can apply to their development workflows</p>
</li>
<li><p>Planning incentives like custom swag or other special recognition that you can offer your most passionate champions</p>
</li>
</ul>
<p>For this phase, you want to consider ways you can encourage potential members to take part in your community and give back to it. Providing them a place to meet, support, and network with like-minded individuals is a given, but offering learning opportunities, social clout, or material benefit can also be powerful.</p>
<p>Planning is important, but execution is everything and that leads us into the next phase of our strategy.</p>
<h2 id="heading-phase-4-execute-and-engage">Phase 4: Execute and Engage</h2>
<p>The ultimate goal of this stage is to build trust and genuine relationships within the community.</p>
<p>Execution involves:</p>
<ul>
<li><p>Sticking to the planned schedule for content publishing and delivering</p>
</li>
<li><p>Setting firm dates and times for events and meetups and delivering</p>
</li>
<li><p>Encouraging growth by guiding new community members from your events and content to your online platforms and integrating them in a welcoming way</p>
</li>
<li><p>Sharing updates and roadmaps to keep the community excited and engaged</p>
</li>
<li><p>Providing opportunities for competition and collaboration within the community</p>
</li>
<li><p>Ensuring consistent staff presence to ensure community members feel heard</p>
</li>
<li><p>Elevating and recognizing active and helpful community members to encourage a sense of pride in their efforts</p>
</li>
</ul>
<p>By executing your strategy and engaging with your audience you are building a strong foundation based on availability, consistency, and demonstrated value. You want your community members to feel proud to contribute to and be a part of your community (while being wary of elitism!), and therefore spontaneously encourage others to join. When done correctly, this creates a powerful feedback loop of growth!</p>
<h2 id="heading-phase-5-measure-and-iterate">Phase 5: Measure and Iterate</h2>
<p>Finally, it's necessary to measure the success of your hard work in relation to your initial goals and go back to Phase 3 to adjust your strategy as needed. Some metrics could include:</p>
<ul>
<li><p>Content: Traffic to your site, social media engagement, downloads of projects</p>
</li>
<li><p>Events: NPS (% of promoters - detractors) from surveys, special signup codes</p>
</li>
<li><p>Online Community Growth: Measure the number of new community members coming from meetups, conferences and other engagements. Bespoke invite codes can be useful here!</p>
</li>
<li><p>Product and tool adoption: Track the usage of your product or tool and identify trends that may suggest an initiative is working</p>
</li>
</ul>
<p>It is vital to take stock and use metrics that map to your initial goals for your community in some way to inform your community engagement strategy. This will enable you to improve your approach over time. Remember that building a sustainable community takes effort, iteration, and consistency but with the right strategy and commitment to your metrics, you will always move toward building a sustainable community.</p>
<h2 id="heading-sustainability-and-beyond">Sustainability and Beyond!</h2>
<p>Sustainability is achieved when community members can take the lead in the initiatives your team has modeled. This can involve:</p>
<ul>
<li><p>Community members publishing tutorials, open-source code snippets, and apps</p>
</li>
<li><p>Community members providing support and sharing resources</p>
</li>
<li><p>Community members volunteering to become moderators</p>
</li>
</ul>
<p>Reaching this point is a huge milestone, and you’ll notice that your team’s role will start to change. While the amount of direct involvement they’ll have with day-to-day tasks like answering support questions will decrease, it is important not to distance yourself from the community and maintain a firm, consistent presence. Your team will take on more management-type duties where they actively look out for and spotlight quality community content, create guidelines for contribution, and keep an eye out for rising stars who can make up the next wave of champions.</p>
<p>However, don’t let the fact that your community has a feedback loop going lull you to sleep: keep your foot on the pedal and continue to foster engagement and growth through your content calendar or other strategies that have led you this far!</p>
<h2 id="heading-thank-you">Thank you!</h2>
<p>We hope reading about our process helps you form strategies for building communities of your own! No two communities are exactly alike, but this general framework can help you stay organized and measure the success of what’s working and what isn’t. If you have any questions or comments, feel free to reach out to us on Twitter <a target="_blank" href="https://twitter.com/janeth_graziani">@janeth_graziani</a> and <a target="_blank" href="https://twitter.com/Hacubu">@Hacubu</a>. Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[From Founder to Freelancer: How I Incorporated a Business and Onboarded My First Client]]></title><description><![CDATA[Hey readers! For those of you who don't know me, I was previously the co-founder and CTO of a startup called Autocode. We built and scaled a fantastic product that reached over 600,000 developers, but after six years, I felt it was time for a change ...]]></description><link>https://blog.jacobscript.dev/from-founder-to-freelancer-how-i-incorporated-a-business-and-onboarded-my-first-client</link><guid isPermaLink="true">https://blog.jacobscript.dev/from-founder-to-freelancer-how-i-incorporated-a-business-and-onboarded-my-first-client</guid><category><![CDATA[Freelancing]]></category><category><![CDATA[freelance]]></category><category><![CDATA[Career]]></category><category><![CDATA[sidehustle]]></category><dc:creator><![CDATA[Jacob Lee]]></dc:creator><pubDate>Fri, 10 Feb 2023 09:28:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676048350599/f34279d8-63e9-4269-8bc0-cfe082e4167c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hey readers! For those of you who don't know me, I was previously the co-founder and CTO of a startup called</em> <a target="_blank" href="https://autocode.com"><em>Autocode</em></a><em>. We built and scaled a fantastic product that reached over</em> <strong><em>600,000 developers</em></strong>, <em>but after six years, I felt it was time for a change of scenery and a new adventure. I've decided to do some writing to document my post-Autocode journey - if you're interested in reading more, follow me here!</em></p>
<p>So why did I decide to become a freelance developer? After making the decision to leave Autocode and looking back at my time there, I realized I was quite interested in seeing how other organizations function - how they're structured and managed, the problems they're solving (both technical and non-technical!), and how they overcome adversity. I also wanted to explore different product areas and meet interesting, passionate people. Freelancing ticked those boxes, and had the added benefit of flexibility and control over my schedule after years of constant late-night product launches and long on-call stretches.</p>
<p>It sounded great in theory, but I quickly found that getting started was more complex than just finding a client, writing some code, and cashing a check.</p>
<p>My first thought was to simply Google <code>how to start consulting</code>. Here's what I saw:</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dhp2yge03emd9gvqpdla.png" alt="Google results for how to start consulting" /></p>
<p>The results included pages of clickbait-y, generic content marketing articles published by companies looking for SEO, as well as an intimidating dropdown that said I should expect getting started to cost <strong>$10k-50k</strong> (it's much, much less!). The actual details of getting started as a freelancer - like how to incorporate, tax implications, how to set up a contract - proved scarce. And that's really too bad, since freelancing opens up opportunities for both full-time work and side-hustles.</p>
<p>Here's how I cut through all the noise, incorporated my business, and onboarded my first paying client!</p>
<p><strong>Sidebar: I am not a lawyer or an accountant - don't take my experience as gospel and do your own research!</strong></p>
<h1 id="heading-deciding-on-a-corporate-structure">Deciding on a Corporate Structure</h1>
<p>Confused by all the information online, I asked my cousin, who had done some freelancing in the past, for some advice on corporate structure. She replied, "Oh, you can just start out by using your Social Security Number!"</p>
<p>Wait, really? Sure, <a target="_blank" href="https://www.ftb.ca.gov/file/personal/filing-situations/self-employed.html">otherwise every part-time Uber and Lyft driver would need to incorporate</a>! This seemed like the easiest approach, but I decided on an LLC for a few reasons (in no specific order of importance):</p>
<ol>
<li><p>The corporation is treated as a <a target="_blank" href="https://en.wikipedia.org/wiki/Corporate_personhood">separate entity</a> from me. In an unlikely scenario where a client sues, my liability is limited in most cases (as the name Limited Liability Corporation implies!). I <em>probably</em> won't lose my house.</p>
</li>
<li><p>It gives the impression of professionalism to clients.</p>
</li>
<li><p>I thought it'd be cool.</p>
</li>
</ol>
<p>There are more complicated structures as well, but since the business would just be me an <strong>LLC seemed like a good balance between ease of management and legal protection in the worst case</strong>. Single-member LLCs can also <a target="_blank" href="https://www.irs.gov/businesses/small-businesses-self-employed/single-member-limited-liability-companies">report income on the owner's tax return</a>, so I still only need to file one tax return.</p>
<h1 id="heading-incorporating-an-llc">Incorporating an LLC</h1>
<p>Note that your experience may vary depending on the state - I filed in California.</p>
<p>After deciding on an LLC, my next step was legally incorporating. As a California resident, I was unsure of the tax implications if I incorporated in a different state, so I chose to incorporate in California. The downside to this is that California has <a target="_blank" href="https://www.ftb.ca.gov/file/business/types/limited-liability-company/index.html#Annual-Tax">a very expensive Annual Tax</a> on LLCs - $800 even if you make no money, and an additional tax if you make more than $250,000 a year!</p>
<p>The good news is that the $800 component of the first year tax is being waived for businesses that incorporate before January 1st, 2024.</p>
<p><em>Side note: I probably made a mistake here - I forgot about</em> <a target="_blank" href="https://stripe.com/atlas"><em>Stripe Atlas</em></a><em>! They charge $500 to incorporate a business in Delaware (which has the most business-friendly laws for some reason), and $100 a year for management after. It's a bargain compared to fees in California, especially since they give Stripe credits that you can use to avoid payment processing fees when invoicing clients! There's a good chance I'll spin down my current LLC before the $800 tax kicks in and redo things in Delaware.</em></p>
<p>This process is run by the California Secretary of State's office. They have a <a target="_blank" href="https://bizfileonline.sos.ca.gov/">surprisingly decent website</a>, and all the filing can be done online.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cn2wmnljevfta8nbjuch.png" alt="The California Secretary of State's Filing Portal" /></p>
<p>There's a section where it'll asked if I was a professional service provider, but this only applies to <a target="_blank" href="https://www.stimmel-law.com/en/articles/professional-corporations-california-definitions-and-practicalities#:~:text=%E2%80%9CProfessional%20services%E2%80%9D%20are%20any%20type,Act%2C%20or%20the%20Osteopathic%20Act.">professions that require a license</a>, and writing software does not (yay?). The process required information like my Social Security Number, a business name, and a business address (like most freelancers, I don't have an office, so I used my current home address) and pay a small fee. A few days later my application was accepted, I had my Articles of Incorporation, and Remora Software, LLC was born!</p>
<p>After incorporation, I had to file a "Statement of Information" within 90 days of incorporation. You can do this from the <a target="_blank" href="https://bizfileonline.sos.ca.gov/">Secretary of State's website</a>, and it's required every two years afterward.</p>
<h1 id="heading-opening-a-business-checking-account">Opening a Business Checking Account</h1>
<p>In software, there's a very important design concept called <a target="_blank" href="https://en.wikipedia.org/wiki/Separation_of_concerns">separation of concerns</a>. it turns out, the same applies to LLCs. It becomes much easier to maintain a company's separate personhood and liability protection if all business income and expenses are separated from personal ones, so this is a must.</p>
<p>Plus, you'll feel like a boss when the bank sends a card with your company name on it.</p>
<p>Opening a business bank account requires an EIN (employer identification number). The IRS assigns EINs, and again, I was pleasantly surprised with how easy the process was. <a target="_blank" href="https://www.irs.gov/businesses/small-businesses-self-employed/apply-for-an-employer-identification-number-ein-online">Everything is online</a>, and I had an EIN a few minutes after applying using information from my LLC's Articles of Incorporation.</p>
<p>You can then transfer money from your business account to your personal account via <a target="_blank" href="https://bench.co/blog/accounting/owners-draw/">owner's draw</a> as often as you'd like - your checking account will act as a record of how much revenue your business is making for tax purposes.</p>
<p>I opened an account with <a target="_blank" href="https://chase.com">Chase</a> as I had used them in the past and they were running a $700 promotion for new accounts that meet certain requirements, but there are plenty of good options out there.</p>
<h1 id="heading-setting-up-a-contract">Setting Up a Contract</h1>
<p>I was fortunate enough to be referred to a small startup interested in hiring someone with my skills. In retrospect, I could have negotiated a much higher rate, but I was eager to get my first client and get a win on the board.</p>
<p>The next step was putting together a contract and figuring out how to get paid. I looked around for solutions and eventually found <a target="_blank" href="https://www.honeybook.com/">Honeybook</a>, a client management platform. The week-long free trial and $1/month introductory rate drew me in, and I found it well-suited for my needs.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/huq3101ckq1otge3rnv1.png" alt="Honeybook Contract Invoice" /></p>
<p>I used Honeybook's provided standard contract template, which I trusted because they've <a target="_blank" href="https://www.crunchbase.com/organization/honeybook">raised almost half a billion dollars</a>. It conveniently included an invoicing page, payment processing, and automatic reminders. They charge a 1.5% fee for ACH transfers, so I may explore using Stripe, a wire transfer, or Zelle in the future, but all in all, it was a convenient and client-friendly way to get paid, though there was some difficulty verifying my payout account because I had only incorporated a few days before.</p>
<p>The initial contract was for two weeks, and I structured payment so that half of the value was due up front and half at the end of the contract. The client signed and sent the money, I got to work, and it was a great feeling to have my first freelance contract in the books!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/Hacubu/status/1620839324358160384">https://twitter.com/Hacubu/status/1620839324358160384</a></div>
<p> </p>
<h1 id="heading-thank-you">Thank You!</h1>
<p>If you've made it this far, thanks for reading! I'm not sure I'll remain a full-time freelancer forever, but incorporating was an extremely rewarding experience and opens up the potential to do part-time consulting on the side. <strong>I'd encourage more developers to give it a shot!</strong> And please let me know in the comments if anything in the article looks amiss.</p>
<p>If you're interested in hiring me, you can read more about me on my <a target="_blank" href="https://jacobscript.dev">personal website</a> - I specialize in cloud architecture/systems design, devops, and backend development, but started my career as a frontend engineer on Google Photos and can contribute to any part of a stack.</p>
<p>To stay up to date with my journey, follow me here or on Twitter <a target="_blank" href="https://twitter.com/hacubu">@Hacubu</a>. Happy hacking!</p>
]]></content:encoded></item></channel></rss>