<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nir Berko</title>
    <description>The latest articles on DEV Community by Nir Berko (@nirberko).</description>
    <link>https://dev.to/nirberko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F82336%2F1043cc2f-2e32-41e2-9105-abc3781e8835.jpeg</url>
      <title>DEV Community: Nir Berko</title>
      <link>https://dev.to/nirberko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nirberko"/>
    <language>en</language>
    <item>
      <title>How I Built an AI-Powered PR Reviewer in 15 Minutes Using Agentform</title>
      <dc:creator>Nir Berko</dc:creator>
      <pubDate>Mon, 19 Jan 2026 06:07:11 +0000</pubDate>
      <link>https://dev.to/nirberko/how-i-built-an-ai-powered-pr-reviewer-in-15-minutes-using-agentform-4l1p</link>
      <guid>https://dev.to/nirberko/how-i-built-an-ai-powered-pr-reviewer-in-15-minutes-using-agentform-4l1p</guid>
      <description>&lt;h2&gt;
  
  
  How I Built an AI-Powered PR Reviewer in 15 Minutes Using Agentform
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;No Python. No complex orchestration. Just declarative configuration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are plenty of AI-powered PR review tools out there, but I wanted to build my own - partly for more control over the review process, and partly just for fun and to learn a new technology. The challenge? Building one traditionally meant writing a lot of imperative code: managing API calls, handling retries, orchestrating workflows, and dealing with error handling. That's a lot of boilerplate for what should be a simple task.&lt;/p&gt;

&lt;p&gt;Using &lt;strong&gt;Agentform&lt;/strong&gt;, I built a fully functional AI PR reviewer in just 15 minutes that runs automatically on GitHub Actions. Here's how I did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Agentform?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/agentform-org/agentform" rel="noopener noreferrer"&gt;Agentform&lt;/a&gt;&lt;/strong&gt; is Infrastructure as Code for AI agents. Instead of writing imperative Python or JavaScript to manage agent state, retries, and tool wiring, Agentform lets you describe your AI agents &lt;strong&gt;declaratively&lt;/strong&gt; using a native schema format.&lt;/p&gt;

&lt;p&gt;Think of it like Terraform, but for AI workflows. You define &lt;em&gt;what&lt;/em&gt; you want, not &lt;em&gt;how&lt;/em&gt; to do it.&lt;/p&gt;

&lt;p&gt;Agentform is an emerging technology currently in active development and early stages, so expect rapid evolution and potential breaking changes. However, it's already powerful enough to build real-world applications like this PR reviewer, and the declarative approach makes it worth exploring even at this stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Agentform?
&lt;/h3&gt;

&lt;p&gt;Traditional approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Lots of boilerplate, error handling, retry logic...
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;review_pr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch_pr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;post_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# handle errors, retries, etc...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agentform approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;workflow&lt;/span&gt; &lt;span class="s2"&gt;"review_pr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"fetch_pr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"analyze"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reviewer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"submit_review"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, readable, and version-controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.12+&lt;/li&gt;
&lt;li&gt;GitHub repository with Actions enabled&lt;/li&gt;
&lt;li&gt;OpenAI API key&lt;/li&gt;
&lt;li&gt;GitHub Personal Access Token (with &lt;code&gt;repo&lt;/code&gt; scope)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Install Agentform CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;agentform-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Create the Project Structure
&lt;/h3&gt;

&lt;p&gt;I organized my Agentform configuration into numbered files for clarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.af/
├── 00-project.af      # Project metadata
├── 01-variables.af    # Input variables
├── 02-providers.af    # LLM provider config
├── 03-servers.af      # MCP server connections
├── 04-capabilities.af # Tool capabilities
├── 05-policies.af     # Budgets and limits
├── 06-agents.af       # AI agent definitions
└── 07-workflows.af    # Workflow orchestration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Define Variables
&lt;/h3&gt;

&lt;p&gt;First, I defined what inputs my workflow needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .af/01-variables.af&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"openai_api_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"OpenAI API key"&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"github_personal_access_token"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub Personal Access Token"&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"owner"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub repository owner"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"repo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub repository name"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"pr_number"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Pull request number to review"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Configure the LLM Provider
&lt;/h3&gt;

&lt;p&gt;I set up OpenAI with GPT-4o:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .af/02-providers.af&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"llm.openai"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;api_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openai_api_key&lt;/span&gt;
  &lt;span class="nx"&gt;default_params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;temperature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;
    &lt;span class="nx"&gt;max_tokens&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="s2"&gt;"gpt4o"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gpt-4o"&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;temperature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Connect to GitHub via MCP
&lt;/h3&gt;

&lt;p&gt;Agentform uses &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; to connect to external services. I configured the GitHub MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .af/03-servers.af&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mcp"&lt;/span&gt;
  &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"stdio"&lt;/span&gt;
  &lt;span class="nx"&gt;command&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"@modelcontextprotocol/server-github"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_personal_access_token&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Define Capabilities
&lt;/h3&gt;

&lt;p&gt;Capabilities are the tools my agent can use. I defined three GitHub operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .af/04-capabilities.af&lt;/span&gt;

&lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="s2"&gt;"get_pr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"get_pull_request"&lt;/span&gt;
  &lt;span class="nx"&gt;side_effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"read"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="s2"&gt;"list_pr_files"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"get_pull_request_files"&lt;/span&gt;
  &lt;span class="nx"&gt;side_effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"read"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="s2"&gt;"create_review"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"create_pull_request_review"&lt;/span&gt;
  &lt;span class="nx"&gt;side_effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"write"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: Set Policies
&lt;/h3&gt;

&lt;p&gt;I added budget controls to prevent runaway costs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .af/05-policies.af&lt;/span&gt;

&lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="s2"&gt;"review_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;budgets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;max_cost_usd_per_run&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.00&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;budgets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;max_capability_calls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;budgets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timeout_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8: Create the Reviewer Agent
&lt;/h3&gt;

&lt;p&gt;Now for the fun part - defining the AI agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .af/06-agents.af&lt;/span&gt;

&lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="s2"&gt;"reviewer"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpt4o&lt;/span&gt;

  &lt;span class="nx"&gt;instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
You are an expert code reviewer. Review pull requests thoroughly.

Focus on:
- Code quality and best practices
- Potential bugs or edge cases
- Performance implications
- Security concerns
- Documentation and readability

Be constructive and specific in your feedback.
Suggest improvements where possible.
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;  &lt;span class="nx"&gt;allow&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get_pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list_pr_files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create_review&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;review_policy&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 9: Orchestrate the Workflow
&lt;/h3&gt;

&lt;p&gt;Finally, I wired everything together in a workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .af/07-workflows.af&lt;/span&gt;

&lt;span class="nx"&gt;workflow&lt;/span&gt; &lt;span class="s2"&gt;"review_pr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch_pr&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"fetch_pr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"call"&lt;/span&gt;
    &lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get_pr&lt;/span&gt;

    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;owner&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;
      &lt;span class="nx"&gt;repo&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;
      &lt;span class="nx"&gt;pull_number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pr_number&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"pr_data"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch_files&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"fetch_files"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"call"&lt;/span&gt;
    &lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list_pr_files&lt;/span&gt;

    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;owner&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;
      &lt;span class="nx"&gt;repo&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;
      &lt;span class="nx"&gt;pull_number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pr_number&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"pr_files"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;analyze&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"analyze"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"llm"&lt;/span&gt;
    &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reviewer&lt;/span&gt;

    &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;pr&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pr_data&lt;/span&gt;
      &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pr_files&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"review"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submit_review&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"submit_review"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"call"&lt;/span&gt;
    &lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create_review&lt;/span&gt;

    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;owner&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;
      &lt;span class="nx"&gt;repo&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;
      &lt;span class="nx"&gt;pull_number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pr_number&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;
      &lt;span class="nx"&gt;event&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"COMMENT"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"result"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="s2"&gt;"end"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"end"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how clean this is? Each step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetches PR data&lt;/strong&gt; from GitHub&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gets the changed files&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asks the AI agent to review&lt;/strong&gt; (with access to PR and file data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submits the review&lt;/strong&gt; as a comment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The data flows naturally through &lt;code&gt;state&lt;/code&gt;, and each step explicitly references the next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10: Set Up GitHub Actions
&lt;/h3&gt;

&lt;p&gt;To automate this, I created a GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/agentform-pr-review.yml&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PR Review&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AI PR Review&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install agentform-cli&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install agentform-cli==0.0.3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run PR Review&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.af&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;agentform run review_pr \&lt;/span&gt;
            &lt;span class="s"&gt;--var openai_api_key=${{ secrets.OPENAI_API_KEY }} \&lt;/span&gt;
            &lt;span class="s"&gt;--var github_personal_access_token=${{ secrets.GITHUB_TOKEN }} \&lt;/span&gt;
            &lt;span class="s"&gt;--var owner=${{ github.repository_owner }} \&lt;/span&gt;
            &lt;span class="s"&gt;--var repo=${{ github.event.repository.name }} \&lt;/span&gt;
            &lt;span class="s"&gt;--var pr_number=${{ github.event.pull_request.number }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added two secrets in GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;: My OpenAI API key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GITHUB_TOKEN&lt;/code&gt;: Automatically provided by GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it! Now every PR automatically gets reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;When a pull request is opened or updated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions triggers&lt;/strong&gt; the workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentform fetches the PR data&lt;/strong&gt; using the GitHub MCP server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentform gets the list of changed files&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI agent reviews&lt;/strong&gt; the code with access to both PR metadata and file contents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The review is posted&lt;/strong&gt; as a comment on the PR&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All orchestration, error handling, and state management is handled by Agentform. I just defined the workflow declaratively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Locally
&lt;/h2&gt;

&lt;p&gt;You can test the workflow locally before pushing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; .af

agentform run review_pr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--var&lt;/span&gt; &lt;span class="nv"&gt;openai_api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--var&lt;/span&gt; &lt;span class="nv"&gt;github_personal_access_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-token"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--var&lt;/span&gt; &lt;span class="nv"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-username"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--var&lt;/span&gt; &lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-repo"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--var&lt;/span&gt; &lt;span class="nv"&gt;pr_number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Love About This Approach
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No boilerplate code&lt;/strong&gt;: Everything is declarative configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version controlled&lt;/strong&gt;: Agent logic is in &lt;code&gt;.af&lt;/code&gt; files, making changes reviewable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safe&lt;/strong&gt;: Agentform validates the schema before execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable&lt;/strong&gt;: Agents and workflows can be reused and combined&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost controlled&lt;/strong&gt;: Built-in budget limits prevent surprises&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-provider&lt;/strong&gt;: Easy to switch between OpenAI, Anthropic, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reusability: Using as a Module
&lt;/h2&gt;

&lt;p&gt;One of the coolest features of Agentform is that you can package your agent configuration as a reusable module. You can easily import this PR reviewer workflow into your own Agentform project using Agentform modules - no need to copy files or duplicate configuration!&lt;/p&gt;

&lt;p&gt;Just reference it directly from your Agentform project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"simple-agentform-module"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/nirberko/agentform-pr-reviewer-example//.af"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;  &lt;span class="c1"&gt;// Git branch name&lt;/span&gt;

  &lt;span class="c1"&gt;// Required parameters - pass through our project variables&lt;/span&gt;
  &lt;span class="nx"&gt;openai_api_key&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openai_api_key&lt;/span&gt;
  &lt;span class="nx"&gt;github_personal_access_token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_token&lt;/span&gt;
  &lt;span class="nx"&gt;owner&lt;/span&gt;                        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;
  &lt;span class="nx"&gt;repo&lt;/span&gt;                         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;
  &lt;span class="nx"&gt;pr_number&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pr_number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Agentform modules make it incredibly easy to share and reuse agent configurations across projects or teams, ensuring everyone is using the same, tested workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customization
&lt;/h2&gt;

&lt;p&gt;Want to change the review style? Edit the agent instructions in &lt;code&gt;06-agents.af&lt;/code&gt;. Need different budget limits? Update &lt;code&gt;05-policies.af&lt;/code&gt;. Want to use a different model? Modify &lt;code&gt;02-providers.af&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All changes are in configuration files - no code changes needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;I now have an AI-powered PR reviewer that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Runs automatically on every PR&lt;/li&gt;
&lt;li&gt;✅ Provides intelligent, constructive feedback&lt;/li&gt;
&lt;li&gt;✅ Is fully version-controlled and maintainable&lt;/li&gt;
&lt;li&gt;✅ Took me 15 minutes to set up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;You can extend this further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add support for different review criteria&lt;/li&gt;
&lt;li&gt;Create multiple reviewer agents for different aspects (security, performance, style)&lt;/li&gt;
&lt;li&gt;Integrate with other MCP servers (databases, APIs, etc.)&lt;/li&gt;
&lt;li&gt;Build more complex multi-agent workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/agentform-org/agentform" rel="noopener noreferrer"&gt;Agentform GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nirberko/agentform-pr-reviewer-example" rel="noopener noreferrer"&gt;Example Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Have you tried Agentform? What AI workflows would you build with it?&lt;/strong&gt; Let me know in the comments! 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>github</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Production-Grade Scraper with Playwright, Chromium, Kubernetes, and AWS</title>
      <dc:creator>Nir Berko</dc:creator>
      <pubDate>Fri, 16 Jan 2026 11:21:00 +0000</pubDate>
      <link>https://dev.to/nirberko/building-a-production-grade-scraper-with-playwright-chromium-kubernetes-and-aws-13oo</link>
      <guid>https://dev.to/nirberko/building-a-production-grade-scraper-with-playwright-chromium-kubernetes-and-aws-13oo</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Recently, I worked on scraping an off-market real estate platform that required authentication, session handling, and dynamic rendering.&lt;/p&gt;

&lt;p&gt;This was not a simple HTTP scraper.&lt;/p&gt;

&lt;p&gt;The site is a single-page application, protected by login, session cookies, and active blocking mechanisms. To make it work reliably in production, I had to combine headless browser automation, Kubernetes, AWS-managed infrastructure, and a deliberate proxy strategy.&lt;/p&gt;

&lt;p&gt;This post walks through the architecture, the key technical decisions, and the lessons learned for anyone building scrapers that need to survive real-world constraints.&lt;/p&gt;




&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;The system was split into two microservices running inside Kubernetes on AWS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scraper Service (Node.js / NestJS)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orchestrating login&lt;/li&gt;
&lt;li&gt;Managing session state&lt;/li&gt;
&lt;li&gt;Fetching listing and detail data&lt;/li&gt;
&lt;li&gt;Parsing and normalizing results&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chromium Service&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A dedicated headless Chromium instance running as a separate pod using the &lt;code&gt;browserless/chrome&lt;/code&gt; image.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scraper service connects to Chromium over &lt;strong&gt;CDP via WebSocket&lt;/strong&gt;, exposed through an internal Kubernetes Service. This separation kept browser concerns isolated and made the system easier to reason about and evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a Headless Browser Was Required
&lt;/h2&gt;

&lt;p&gt;A headless browser was unavoidable for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform requires authentication and session cookies
&lt;/li&gt;
&lt;li&gt;The site is a SPA that relies heavily on client-side rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pure HTTP scraping could not reliably establish or maintain a valid session. Playwright was used to perform login, manage cookies and local storage, and act as the source of truth for the authentication state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Playwright
&lt;/h2&gt;

&lt;p&gt;Playwright turned out to be a strong fit for this use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reliable handling of modern SPAs
&lt;/li&gt;
&lt;li&gt;Clean APIs for waiting on navigation and DOM readiness
&lt;/li&gt;
&lt;li&gt;Stable session and cookie management
&lt;/li&gt;
&lt;li&gt;Native support for connecting to a remote Chromium instance over CDP
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, it significantly reduced flakiness around login flows and dynamic rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting to Remote Chromium in Kubernetes
&lt;/h2&gt;

&lt;p&gt;Chromium ran behind an internal Kubernetes Service.&lt;/p&gt;

&lt;p&gt;The scraper connected using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectOverCDP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//&amp;lt;chromium-service&amp;gt;:&amp;lt;port&amp;gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host and port were injected via environment variables, which allowed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean separation between dev, staging, and production
&lt;/li&gt;
&lt;li&gt;No hard-coded endpoints
&lt;/li&gt;
&lt;li&gt;Easy switching between local and remote browser execution
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup was stable and did not require reconnect logic or special handling.&lt;/p&gt;




&lt;h2&gt;
  
  
  AWS Infrastructure Choices
&lt;/h2&gt;

&lt;p&gt;Although this system was Kubernetes-first, AWS services played a key role in making it production-ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon EKS
&lt;/h3&gt;

&lt;p&gt;The entire system ran on Amazon EKS, which provided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A managed Kubernetes control plane
&lt;/li&gt;
&lt;li&gt;Predictable networking and service discovery
&lt;/li&gt;
&lt;li&gt;Clean separation between scraper and browser workloads
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EKS made it straightforward to run a browser-heavy workload while still benefiting from Kubernetes primitives.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Secrets Manager
&lt;/h3&gt;

&lt;p&gt;All sensitive configurations, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform credentials
&lt;/li&gt;
&lt;li&gt;Proxy credentials
&lt;/li&gt;
&lt;li&gt;Environment-specific secrets
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;was stored in AWS Secrets Manager and injected into pods via environment variables. This avoided hardcoding secrets into images or manifests and enabled clean separation between environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  IAM Roles for Service Accounts (IRSA)
&lt;/h3&gt;

&lt;p&gt;Pods accessed AWS resources using IAM roles attached to Kubernetes service accounts. This eliminated static AWS credentials and followed least-privilege principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon CloudWatch
&lt;/h3&gt;

&lt;p&gt;Logs from the scraper service were shipped to CloudWatch. This was critical for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging silent failures
&lt;/li&gt;
&lt;li&gt;Identifying where blocking occurred (login vs data fetch)
&lt;/li&gt;
&lt;li&gt;Understanding retry behavior over time
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scraping systems fail quietly. Centralized logging was essential.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reality of Scraping Blocked Websites
&lt;/h2&gt;

&lt;p&gt;In production scraping, parsing data is rarely the hard part.&lt;br&gt;&lt;br&gt;
Getting access is.&lt;/p&gt;

&lt;p&gt;Modern platforms actively block automated traffic using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting
&lt;/li&gt;
&lt;li&gt;IP reputation checks
&lt;/li&gt;
&lt;li&gt;Blocking cloud datacenter IP ranges
&lt;/li&gt;
&lt;li&gt;Flagging abnormal login or navigation patterns
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without mitigation, even a well-written scraper may work once and then silently stop.&lt;/p&gt;

&lt;p&gt;This is where proxies become unavoidable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Proxy Strategy: Necessary but Expensive
&lt;/h2&gt;

&lt;p&gt;For authenticated and high-value platforms, proxies are not about scale. They are about survival.&lt;/p&gt;

&lt;p&gt;Without proxies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login attempts get blocked quickly
&lt;/li&gt;
&lt;li&gt;Sessions are invalidated
&lt;/li&gt;
&lt;li&gt;Requests return partial or empty responses
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Residential or ISP-grade proxies improve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP reputation
&lt;/li&gt;
&lt;li&gt;Session stability
&lt;/li&gt;
&lt;li&gt;Retry success rates
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is cost and operational complexity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use Proxies Only Where You Must
&lt;/h2&gt;

&lt;p&gt;One key lesson was to treat proxies as an &lt;strong&gt;expensive resource&lt;/strong&gt;, not the default path.&lt;/p&gt;

&lt;p&gt;In this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proxies were mainly used for browser-based actions, especially login
&lt;/li&gt;
&lt;li&gt;Once a valid session was established, most data fetching was done via direct HTTP requests using session cookies
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced proxy traffic and cost
&lt;/li&gt;
&lt;li&gt;Improved speed
&lt;/li&gt;
&lt;li&gt;Lowered exposure to unnecessary blocking
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Key Optimization: Browser for Login, HTTP for Data
&lt;/h2&gt;

&lt;p&gt;After login, cookies were extracted from the browser context and reused for direct HTTP requests from Node.js.&lt;/p&gt;

&lt;p&gt;This allowed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetching JSON listing data without rendering pages
&lt;/li&gt;
&lt;li&gt;Fetching HTML detail pages directly
&lt;/li&gt;
&lt;li&gt;Full control over headers, retries, and backoff logic
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Much faster than rendering each page in Chromium
&lt;/li&gt;
&lt;li&gt;More stable than page-by-page navigation
&lt;/li&gt;
&lt;li&gt;Lower proxy and browser costs
&lt;/li&gt;
&lt;li&gt;Easier retry handling
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser became an authentication tool, not a data-fetching bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Single-Tenant Design (By Choice)
&lt;/h2&gt;

&lt;p&gt;The scraper was intentionally designed as &lt;strong&gt;single-tenant&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One browser instance
&lt;/li&gt;
&lt;li&gt;One context and page
&lt;/li&gt;
&lt;li&gt;Shared login state
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is optimized for speed and simplicity.&lt;/p&gt;

&lt;p&gt;If scaling to multi-tenancy, the next steps would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introducing a job queue (for example, SQS)
&lt;/li&gt;
&lt;li&gt;Explicit concurrency limits
&lt;/li&gt;
&lt;li&gt;Separate browser contexts per job
&lt;/li&gt;
&lt;li&gt;Tighter resource control per pod
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this use case, simplicity was the right tradeoff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Kubernetes Resource Considerations
&lt;/h2&gt;

&lt;p&gt;Chromium ran reliably without explicit CPU or memory tuning.&lt;/p&gt;

&lt;p&gt;For higher load, best practices would include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit CPU and memory requests and limits
&lt;/li&gt;
&lt;li&gt;Increasing &lt;code&gt;/dev/shm&lt;/code&gt; using &lt;code&gt;emptyDir&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Monitoring browser memory growth over time
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These become critical as concurrency increases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scraping Is an Arms Race
&lt;/h2&gt;

&lt;p&gt;There is no “set it and forget it” scraper.&lt;/p&gt;

&lt;p&gt;Sites change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login flows
&lt;/li&gt;
&lt;li&gt;Required headers
&lt;/li&gt;
&lt;li&gt;JavaScript behavior
&lt;/li&gt;
&lt;li&gt;Bot detection rules
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A scraper that works today can break tomorrow without any code changes.&lt;/p&gt;

&lt;p&gt;Scraping should be treated as a system, not a script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expect failures
&lt;/li&gt;
&lt;li&gt;Build retries
&lt;/li&gt;
&lt;li&gt;Monitor success rates
&lt;/li&gt;
&lt;li&gt;Adapt proxy strategy over time
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Scraping modern web applications is less about HTML parsing and more about system design.&lt;/p&gt;

&lt;p&gt;Using a headless browser only where it is truly required, combined with direct HTTP requests wherever possible, provides the best balance between reliability, speed, and cost.&lt;/p&gt;

&lt;p&gt;If scraping is a core dependency, the real question is not &lt;em&gt;can&lt;/em&gt; you scrape the data, but whether the data is valuable enough to justify the ongoing operational complexity.&lt;/p&gt;

&lt;p&gt;That question should be answered early.&lt;/p&gt;

</description>
      <category>scraping</category>
      <category>playwright</category>
      <category>kubernetes</category>
      <category>aws</category>
    </item>
    <item>
      <title>Authentication using AuthGrid</title>
      <dc:creator>Nir Berko</dc:creator>
      <pubDate>Fri, 05 Feb 2021 18:11:05 +0000</pubDate>
      <link>https://dev.to/nirberko/authentication-using-authgrid-9k7</link>
      <guid>https://dev.to/nirberko/authentication-using-authgrid-9k7</guid>
      <description>&lt;p&gt;&lt;strong&gt;Notice that AuthGrid is not yet ready for production environment and is still in progress!&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is AuthGrid
&lt;/h3&gt;

&lt;p&gt;AuthGrid is an end-to-end open source authentication provider (both server-side and client-side) that lets you focus on your app and skip the boring and time-wasting authentication development. Forget about creating that login and register pages again and again. forget about creating a profile page, user settings, webhooks, integrations, audits, and more!&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use AuthGrid?
&lt;/h3&gt;

&lt;p&gt;First of all, AuthGrid currently supporting only express.js for the backend, React in the frontend, and mongoose as a database, but more frameworks and databases will be supported in the future!&lt;/p&gt;

&lt;p&gt;for our example, I'll be building a dashboard application using the express.js framework for my backend, MongoDB (using mongoose) for the database, and React for the client-side.&lt;/p&gt;

&lt;h4&gt;
  
  
  installing AuthGrid for backend
&lt;/h4&gt;

&lt;p&gt;we need to add AuthGrid middleware to our express backend, and also to install the database driver that matches our needs, in that case, we need to install the AuthGrid mongoose driver&lt;/p&gt;

&lt;p&gt;Let's install both AuthGrid client the mongoose driver to our express backend &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @authgrid/client @authgrid/mongoose-driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;or using yarn&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @authgrid/client @authgrid/mongoose-driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  Usage
&lt;/h4&gt;

&lt;p&gt;first of all, we need of course to configure mongoose with our mongodb database connection uri, for example:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await mongoose.connect(String('mongodb://localhost:27017/my_dashboard'));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;then, implementing AuthGrid is very simple, just add AuthGrid client middleware at the beginning of your express app:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import AuthGrid from '@authgrid/client';
import MongooseDriver from '@authgrid/mongoose-driver';

.....

app.use(  
  '/authgrid',  
  Authgrid({  
    driver: MongooseDriver(),  
    tokenSecret: ...,  
    refreshTokenSecret: ...,  
    mailer: ...,  
  })  
); 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;notice that AuthGrid requires some options, so let's explore than now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Driver&lt;/strong&gt; is the way AuthGrid client communicating with our database. in our example, we are using MongoDB, so we need to import &lt;code&gt;@authgrid/mongoose-driver&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;tokenSecret&lt;/strong&gt; and refreshTokenSecret are very important and are using by AuthGrid to create hashed tokens for authenticating the users.&lt;br&gt;
&lt;strong&gt;mailer&lt;/strong&gt; is the way AuthGrid can send emails, this function will be called everytime AuthGird wants to send an email, for example, this is how im using SendGrid as my email provider:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const mailer = async ({ to, subject, html }) =&amp;gt;  
  sgMail.send({  
    to,  
    from: 'myemail@gmail.com',  
    subject,  
    html,  
  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;we almost done,&lt;br&gt;
last thing we need to do is to protect our api using the withAutentication middleware provided by AuthGrid.&lt;br&gt;
this is how we do it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.get('/get-user-details', withAuthentication(), (req, res) =&amp;gt; {  
  res.json(req.user);  
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  installing AuthGrid for React.js
&lt;/h4&gt;

&lt;p&gt;So now that our express.js backend is ready and protected, lets move to the client-side.&lt;br&gt;
AuthGrid providing us also a very powerful client-side authentication components and state management.&lt;br&gt;
let's see how we can use those pre-made components by AuthGrid.&lt;/p&gt;

&lt;p&gt;First, we need to install the AuthGrid package for react&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @authgrid/react-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;or using yarn&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @authgrid/react-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  usage
&lt;/h4&gt;

&lt;p&gt;so now that we have the AuthGrid react-core package installed, we need to warp our whole component with the AuthGrid provider.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React from 'react';  
import ReactDOM from 'react-dom';  
import { BrowserRouter, Switch } from 'react-router-dom';  
import { AuthgridProvider, ProtectedRoute, useAuth } from '@authgrid/react-core';  

const context = {  
  baseUrl: 'http://localhost:8080',  
};  

const Home = () =&amp;gt; {  
  const { user } = useAuth();  

  return &amp;lt;div&amp;gt;{user?.email}&amp;lt;/div&amp;gt;;  
}; 

ReactDOM.render(  
  &amp;lt;BrowserRouter&amp;gt;  
    &amp;lt;AuthgridProvider context={context}&amp;gt;  
      &amp;lt;Switch&amp;gt;
        &amp;lt;ProtectedRoute path="/" component={Home} /&amp;gt;  
      &amp;lt;/Switch&amp;gt;
    &amp;lt;/AuthgridProvider&amp;gt;
  &amp;lt;/BrowserRouter&amp;gt;,  
  document.getElementById('root')  
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;as you can see, we need to provide AuthGrid the base URL for our server-side, this is very important so AuthGrid can know where to send the fetch requests.&lt;/p&gt;

&lt;p&gt;also, for protecting routes only for authenticated users we can import the &lt;code&gt;ProtectedRoute&lt;/code&gt; component. now, when a user wants to enter that route, he must log in before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's it, we are done!&lt;/strong&gt;, now, let's check our application and see that everything is working.&lt;br&gt;
when you enter your app, you should see the AuthGrid login screen (don't forget to use the &lt;code&gt;ProtectedRoute&lt;/code&gt; component)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flopa0tbgr05x4q63mf32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flopa0tbgr05x4q63mf32.png" alt="AuthGrid" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can now register and login into your application!&lt;/p&gt;

&lt;p&gt;Keep in mind that AuthGird is still at work and can be a bit buggy, and a lot of features are missing and will be added in the future with the help of the community.&lt;br&gt;
so I'm not recommending using this package &lt;strong&gt;yet&lt;/strong&gt;, follow the repo to be updated when AuthGrid is ready to use in production environments&lt;/p&gt;

&lt;p&gt;AuthGrid is looking for contributors (and that also the reason I published this post :)) so contact me if you are interesting :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/authgrid/authgrid" rel="noopener noreferrer"&gt;https://github.com/authgrid/authgrid&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lot of features are yet to come!:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Profile and settings page&lt;/li&gt;
&lt;li&gt;Audits logs&lt;/li&gt;
&lt;li&gt;Webhooks&lt;/li&gt;
&lt;li&gt;Integrations&lt;/li&gt;
&lt;li&gt;User management&lt;/li&gt;
&lt;li&gt;and much more...&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>react</category>
      <category>authentication</category>
    </item>
  </channel>
</rss>
