By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: How to Build an AI-Driven Loan Approval Workflow With Symfony 7.4 and Symfony AI | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > How to Build an AI-Driven Loan Approval Workflow With Symfony 7.4 and Symfony AI | HackerNoon
Computing

How to Build an AI-Driven Loan Approval Workflow With Symfony 7.4 and Symfony AI | HackerNoon

News Room
Last updated: 2026/02/16 at 6:53 PM
News Room Published 16 February 2026
Share
How to Build an AI-Driven Loan Approval Workflow With Symfony 7.4 and Symfony AI | HackerNoon
SHARE

In the rapidly evolving landscape of 2026, “AI-first” is no longer a buzzword — it is an architectural requirement. For fintech institutions, the ability to automate credit decisions while maintaining strict compliance is the holy grail

In this deep dive, we will build a production-grade Bank Loan Approval Workflow. We won’t just move entities from “Draft” to “Approved.” We will inject a cognitive layer into the state machine using symfony/workflow and symfony/ai-bundle. Our system will automatically score loan applications and dynamically route them: high-scoring applications get instant approval, risky ones get rejected and borderline cases are routed to human underwriters.

The Architecture

We are building a Score-Driven State Machine.

Traditional workflows are linear or user-driven. Ours is agentic.

  1. Submission: User submits a loan application.
  2. AI Analysis: A dedicated AI Agent analyzes the applicant’s raw data (income, debt, history) against a “Risk Policy” prompt.
  3. Scoring: The AI returns a structured score (0–100) and a reasoning summary.
  4. Dynamic Routing: n Score > 80: Auto-Approve. n Score < 40: Auto-Reject. n 40–80: Transition to manual_review.

The Stack

  • PHP 8.4: For utilizing the new Property Hooks and native HTML5 parsing if needed.
  • Symfony 7.4: The LTS core.
  • symfony/workflow: Managing the state lifecycle.
  • symfony/ai-bundle: The integration layer for LLMs (OpenAI, Anthropic, or local models).

Project Setup and Prerequisites

First, ensure you have the Symfony CLI and PHP 8.4 installed. We will create a new skeleton project and install our dependencies.

symfony new bank_approval --webapp --version=7.4
cd bank_approval

Installing Dependencies

We need the workflow component and the AI bundle. Note that since mid-2025, symfony/ai-bundle has been the standard for AI integration.

composer require symfony/workflow symfony/ai-bundle symfony/http-client

We assume you have an OpenAI API key or similar for the AI platform configuration.

Configuration

**AI Configuration (config/packages/ai.yaml)
We will configure a “Risk Agent” specifically designed for financial analysis. We use gpt-4o (or the latest equivalent available in 2026) for its reasoning capabilities.

ai:
    platform:
        openai:
            api_key: '%env(OPENAI_API_KEY)%'

    agent:
        risk_officer:
            model: 'gpt-4o'
            prompt:
                file: '%kernel.project_dir%/tools/prompt/riskManager.txt'

Workflow Configuration (config/packages/workflow.yaml)
We define a workflow named 
loan_approval**.

framework:
    workflows:
        loan_approval:
            type: 'state_machine'
            audit_trail:
                enabled: true
            marking_store:
                type: 'method'
                property: 'status'
            supports:
                - AppEntityLoanApplication
            initial_marking: draft

            places:
                - draft
                - processing_score
                - manual_review
                - approved
                - rejected

            transitions:
                submit:
                    from: draft
                    to: processing_score

                auto_approve:
                    from: processing_score
                    to: approved

                auto_reject:
                    from: processing_score
                    to: rejected

                refer_to_underwriter:
                    from: processing_score
                    to: manual_review

                underwriter_approve:
                    from: manual_review
                    to: approved

                underwriter_reject:
                    from: manual_review
                    to: rejected

The Domain Layer

We need an entity that holds the data and the state. We’ll use PHP 8.4 attributes for mapping.

namespace AppEntity;

use AppRepositoryLoanApplicationRepository;
use DoctrineDBALTypesTypes;
use DoctrineORMMapping as ORM;

#[ORMEntity(repositoryClass: LoanApplicationRepository::class)]
class LoanApplication
{
    #[ORMId]
    #[ORMGeneratedValue]
    #[ORMColumn]
    private ?int $id = null;

    #[ORMColumn(length: 255)]
    private ?string $applicantName = null;

    #[ORMColumn]
    private ?int $annualIncome = null;

    #[ORMColumn]
    private ?int $requestedAmount = null;

    #[ORMColumn]
    private ?int $totalMonthlyDebt = null;

    // The Workflow Marking
    #[ORMColumn(length: 50)]
    private string $status="draft";

    // AI Scoring Results
    #[ORMColumn(type: Types::INTEGER, nullable: true)]
    private ?int $riskScore = null;

    #[ORMColumn(type: Types::TEXT, nullable: true)]
    private ?string $aiReasoning = null;

    public function __construct(string $name, int $income, int $amount, int $monthlyDebt)
    {
        $this->applicantName = $name;
        $this->annualIncome = $income;
        $this->requestedAmount = $amount;
        $this->totalMonthlyDebt = $monthlyDebt;
    }

    // Getters and Setters...

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getApplicantName(): ?string
    {
        return $this->applicantName;
    }

    public function setApplicantName(string $applicantName): static
    {
        $this->applicantName = $applicantName;

        return $this;
    }

    public function getAnnualIncome(): ?int
    {
        return $this->annualIncome;
    }

    public function setAnnualIncome(int $annualIncome): static
    {
        $this->annualIncome = $annualIncome;

        return $this;
    }

    public function getRequestedAmount(): ?int
    {
        return $this->requestedAmount;
    }

    public function setRequestedAmount(int $requestedAmount): static
    {
        $this->requestedAmount = $requestedAmount;

        return $this;
    }

    public function getTotalMonthlyDebt(): ?int
    {
        return $this->totalMonthlyDebt;
    }

    public function setTotalMonthlyDebt(int $totalMonthlyDebt): static
    {
        $this->totalMonthlyDebt = $totalMonthlyDebt;

        return $this;
    }

    public function getStatus(): string
    {
        return $this->status;
    }

    public function setStatus(string $status): void
    {
        $this->status = $status;
    }

    public function setAiResult(int $score, string $reasoning): void
    {
        $this->riskScore = $score;
        $this->aiReasoning = $reasoning;
    }

    public function getRiskScore(): ?int
    {
        return $this->riskScore;
    }

    public function getAiReasoning(): ?string
    {
        return $this->aiReasoning;
    }

    // Calculated fields for the AI context
    public function getDtiRatio(): float
    {
        $monthlyIncome = $this->annualIncome / 12;
        if ($monthlyIncome === 0) return 100.0;
        return ($this->totalMonthlyDebt / $monthlyIncome) * 100;
    }
}

The AI Scoring Service

This is the core of our “Intelligent Workflow.” We will create a service that formats the entity data into a prompt, sends it to our configured risk_officer agent and parses the JSON response.

namespace AppService;

use AppEntityLoanApplication;
use SymfonyAIAgentAgentInterface;
use SymfonyAIPlatformMessageMessageBag;
use SymfonyComponentDependencyInjectionAttributeTarget;
use SymfonyAIPlatformMessageUserMessage;
use SymfonyAIPlatformMessageContentText;

readonly class LoanScorer
{
    public function __construct(
        #[Target('risk_officer')]
        private AgentInterface $agent
    ) {}

    /**
     * @return array{score: int, reasoning: string}
     */
    public function scoreApplication(LoanApplication $loan): array
    {
        // 1. Construct the context for the AI
        $context = sprintf(
            "Applicant: %s
Annual Income: $%d
Requested Amount: $%d
Monthly Debt: $%d
Calculated DTI: %.2f%%",
            $loan->getApplicantName(),
            $loan->getAnnualIncome(),
            $loan->getRequestedAmount(),
            $loan->getTotalMonthlyDebt(),
            $loan->getDtiRatio()
        );

        // 2. Create the message
        $message = new UserMessage(new Text($context));

        // 3. Call the AI Agent
        // In Symfony 7.4/AI Bundle, we call the agent which handles the platform communication
        $response = $this->agent->call(
            new MessageBag($message)
        );

        // 4. Parse the output
        // Ideally, we would use Structured Outputs (JSON mode) supported by the bundle
        $content = $response->getContent();

        return $this->parseJson($content);
    }

    private function parseJson(string $content): array
    {
        // The AI might wrap the JSON in a markdown code block. Let's extract it.
        if (preg_match('/```jsons*({.*?})s*```/s', $content, $matches)) {
            $jsonContent = array_pop($matches);
        } else {
            // If no markdown block is found, assume the content is already a JSON string.
            $jsonContent = $content;
        }

        $data = json_decode($jsonContent, true);

        if (!isset($data['score']) || !is_int($data['score']) || !isset($data['reasoning']) || !is_string($data['reasoning'])) {
            throw new RuntimeException('AI returned invalid or malformed JSON format: ' . $content);
        }

        return $data;
    }
}

The Workflow Automator

Now we need the logic that connects the Scorer to the Workflow. This service triggers the transitions based on the score.

namespace AppService;

use AppEntityLoanApplication;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentWorkflowWorkflowInterface;
use PsrLogLoggerInterface;

readonly class LoanAutomationService
{
    public function __construct(
        private WorkflowInterface      $loanApprovalStateMachine,
        private LoanScorer             $scorer,
        private EntityManagerInterface $entityManager,
        private LoggerInterface        $logger
    ) {}

    public function processApplication(LoanApplication $loan): void
    {
        // 1. Verify we are in the correct state
        if ($loan->getStatus() !== 'processing_score') {
            return;
        }

        $this->logger->info("Starting AI scoring for Loan #{$loan->getId()}");

        // 2. Get the AI Score
        try {
            $result = $this->scorer->scoreApplication($loan);

            // Update entity with results
            $loan->setAiResult($result['score'], $result['reasoning']);
            $this->entityManager->flush();

            $score = $result['score'];
            $this->logger->info("AI Score generated: {$score}");

            // 3. Determine and Apply Transition
            $transition = $this->determineTransition($score);

            if ($this->loanApprovalStateMachine->can($loan, $transition)) {
                $this->loanApprovalStateMachine->apply($loan, $transition);
                $this->entityManager->flush();
                $this->logger->info("Applied transition: {$transition}");
            } else {
                $this->logger->error("Transition {$transition} blocked for Loan #{$loan->getId()}");
            }

        } catch (Exception $e) {
            $this->logger->error("AI Scoring failed: " . $e->getMessage());
            // Fallback: Default to manual review on error
            if ($this->loanApprovalStateMachine->can($loan, 'refer_to_underwriter')) {
                $this->loanApprovalStateMachine->apply($loan, 'refer_to_underwriter');
                $this->entityManager->flush();
            }
        }
    }

    private function determineTransition(int $score): string
    {
        return match (true) {
            $score >= 80 => 'auto_approve',
            $score < 40  => 'auto_reject',
            default      => 'refer_to_underwriter',
        };
    }
}

Wiring it with Events

To make this seamless, we want the automation to trigger immediately after the user submits the application. We can use a Workflow Event Listener. When the loan enters the processing_score state (via the submit transition), we trigger the automation.

In a high-scale real-world app, you would dispatch a Symfony Messenger message here to handle the AI call asynchronously. For this example, we will do it synchronously to keep the code focused on logic.

namespace AppEventListenerWorkflow;

use AppEntityLoanApplication;
use AppMessageScoreLoanApplication;
use SymfonyComponentEventDispatcherAttributeAsEventListener;
use SymfonyComponentMessengerMessageBusInterface;
use SymfonyComponentWorkflowEventEvent;

readonly class LoanScoringListener
{
    public function __construct(
        private MessageBusInterface $bus
    ) {}

    /**
     * Listen to the 'entered' event for the 'processing_score' place.
     * Event name format: workflow.[workflow_name].entered.[place_name]
     */
    #[AsEventListener('workflow.loan_approval.entered.processing_score')]
    public function onProcessingScore(Event $event): void
    {
        $subject = $event->getSubject();

        if (!$subject instanceof LoanApplication) {
            return;
        }

        // Trigger the AI Automation asynchronously
        $this->bus->dispatch(new ScoreLoanApplication($subject->getId()));
    }
}

The Controller

Finally, let’s build a controller to simulate the submission.

namespace AppController;

use AppDTOLoanApplicationInput;
use AppEntityLoanApplication;
use DoctrineORMEntityManagerInterface;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpKernelAttributeMapRequestPayload;
use SymfonyComponentRoutingAttributeRoute;
use SymfonyComponentWorkflowWorkflowInterface;

#[Route('/api/loan')]
class LoanController extends AbstractController
{
    #[Route('/submit', methods: ['POST'])]
    public function submit(
        #[MapRequestPayload] LoanApplicationInput $data,
        WorkflowInterface $loanApprovalStateMachine,
        EntityManagerInterface $em
    ): JsonResponse
    {
        // 1. Create Entity from validated DTO
        $loan = new LoanApplication(
            $data->name,
            $data->income,
            $data->amount,
            $data->debt
        );

        $em->persist($loan);
        $em->flush(); // Save as draft first

        // 2. Apply 'submit' transition
        // This moves state to 'processing_score'
        // Which triggers our Listener -> which dispatches a message
        if ($loanApprovalStateMachine->can($loan, 'submit')) {
            $loanApprovalStateMachine->apply($loan, 'submit');
            $em->flush();
        }

        return $this->json([
            'id' => $loan->getId(),
            'status' => $loan->getStatus(),
            'message' => 'Loan application submitted and is being processed.'
        ]);
    }
}

Verification

  1. Configure API Key: Ensure OPENAIAPIKEY is set in .env.local.
  2. Start Server: symfony server:start.
  3. Send Request: Use curl or Postman.

Request (High Risk):

curl -X POST https://127.0.0.1:8000/api/loan/submit 
  -H "Content-Type: application/json" 
  -d '{"name": "Risk Taker", "income": 30000, "amount": 50000, "debt": 2000}'

Expected Response:

{
  "id": 1,
  "status": "rejected",
  "risk_score": 15,
  "ai_reasoning": "The applicant has a Debt-to-Income ratio exceeding 80%..."
}

Request (Low Risk):

curl -X POST https://127.0.0.1:8000/api/loan/submit 
  -H "Content-Type: application/json" 
  -d '{"name": "Safe Saver", "income": 120000, "amount": 10000, "debt": 500}'

Expected Response:

{
  "id": 2,
  "status": "approved",
  "risk_score": 92,
  "ai_reasoning": "The applicant demonstrates excellent financial health with a DTI below 10%..."
}

Conclusion

We have successfully integrated Generative AI into a deterministic business process. This pattern leverages the best of both worlds:

  1. Symfony Workflow: Provides the reliability, audit trails and strict state management required for banking.
  2. Symfony AI: Provides the nuanced decision-making capability that previously required human intervention.

This architecture scales. You can introduce new agents for fraud detection, document analysis (using OCR tools in symfony/ai-bundle), or regulatory compliance checks, all orchestrated within the same transparent workflow system.

Source Code: You can find the full implementation and follow the project’s progress on GitHub: [https://github.com/mattleads/AIBankApproval]

Let’s Connect!

If you found this helpful or have questions about the implementation, I’d love to hear from you. Let’s stay in touch and keep the conversation going across these platforms:

  • LinkedIn: [https://www.linkedin.com/in/matthew-mochalkin/]
  • X (Twitter): [https://x.com/MattLeads]
  • Telegram: [https://t.me/MattLeads]
  • GitHub: [https://github.com/mattleads]

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Here’s How Much A 2021 Tesla Model Y Has Depreciated In Just 5 Years – BGR Here’s How Much A 2021 Tesla Model Y Has Depreciated In Just 5 Years – BGR
Next Article 65-inch TCL QM6K Series Smart Google TV: 9.99 65-inch TCL QM6K Series Smart Google TV: $529.99
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Best DJI deal: Save 16% on the DJI Flip with RC 2
Best DJI deal: Save 16% on the DJI Flip with RC 2
News
DeepSeek reveals cost-cutting methods for V3 large model training in new paper · TechNode
DeepSeek reveals cost-cutting methods for V3 large model training in new paper · TechNode
Computing
Waking up at night? These are the gadgets our sleep testers swear by for falling back to sleep fast
Waking up at night? These are the gadgets our sleep testers swear by for falling back to sleep fast
News
This New Chinese Robot Brings The Uncanny Valley To Your Home – BGR
This New Chinese Robot Brings The Uncanny Valley To Your Home – BGR
News

You Might also Like

DeepSeek reveals cost-cutting methods for V3 large model training in new paper · TechNode
Computing

DeepSeek reveals cost-cutting methods for V3 large model training in new paper · TechNode

1 Min Read
Starbucks’ China rival Luckin Coffee to open first US store in New York City · TechNode
Computing

Starbucks’ China rival Luckin Coffee to open first US store in New York City · TechNode

1 Min Read
Zeekr’s privatization will save “several billion yuan” in R&D · TechNode
Computing

Zeekr’s privatization will save “several billion yuan” in R&D · TechNode

4 Min Read
Xiaomi unveils Xuanjie O1, a self-developed phone chip to launch in late May
Computing

Xiaomi unveils Xuanjie O1, a self-developed phone chip to launch in late May

3 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?