IBM Developer

Article

Reimagining workflows with agentic AI

Using CrewAI to implement an agentic workflow with IBM Granite models served by OpenShift AI or watsonx.ai

By Jorge Gonzalez Orozco

The emergence of agentic workflows represents a paradigm shift in artificial intelligence, where systems autonomously plan and execute complex tasks with minimal human supervision. Today's AI landscape is witnessing a veritable gold rush of agentic frameworks, with new architectures and approaches materializing at a dizzying pace. Every week brings announcements of novel systems promising revolutionary capabilities—from AutoGen and MetaGPT to LangGraph, BeeAI, CrewAI, and countless others.

Organizations and researchers are frantically positioning themselves in this rapidly evolving space, each claiming superior reasoning abilities, tool manipulation, or planning strategies. This relentless innovation cycle creates both extraordinary opportunities and significant challenges for practitioners attempting to evaluate, adopt, and integrate these frameworks into practical applications before they become outdated by next week's breakthrough.

This article introduces a financial use case implementation for an agentic workflow with the CrewAI agentic framework and using IBM Granite models served by OpenShift AI or watsonx.ai.

Agentic workflow for personal loan origination

Let’s explore the implementation of a “Personal loan origination” use case for implementing an AI agentic workflow. Instead of creating a chatbot interface for this agentic workflow, this agentic workflow will be a back-end system integrated into the bank’s general systems. Existing web or mobile interfaces for the bank can be the front-end for this workflow. It is assumed that the individual requesting a loan is an existing customer of the bank.

This workflow will target personal loan scenarios (for example, a car loan) for a retail bank. Such workflows could address problems such as compliance by leveraging task-specific AI agents. These agents could radically improve and add efficiency to every step in the loan origination process.

Anticipated as a short-timed flow, the intent is to provide a loan approval request answer in less than 30 seconds. It is presumed that at some point the workflow could reach an exception state (that is, an AI agent times out or provides an invalid response). Only for these exception states or edge scenarios would the workflow reach the help of a human (such as the bank’s Personal Loan Operations officer). The human-in-the-loop feature has not been implemented in this PoC.

The job roles impacted by this workflow include the back-end positions associated with the processing of personal loans. This new automated workflow is expected to enhance the customer experience by reducing loan processing times and adding explainability to the loan response from the bank.

Agentic workflow design

The following figure shows the overall design of an agentic workflow. From left to right, the workflow will be triggered by a loan request from a bank customer and eventually provide a response. There are four agents with different skills and specializations. These agents have access to tools and context knowledge in order to do their job.

Best practice: Your agentic workflow will achieve better performance by having each agent focus on a single narrow activity or task.

Design of an agentic workflow

The loan approval agentic workflow consists of these agents:

  • Risk Analyst Agent. This agent is responsible for validating the financial risk of providing a loan product to a customer including the ability to pay back the loan. Tools used: Different cloud services such as Jumio, Sanctions.io, and so on will provide ID verification, background checks, and related information to support the creation of the customer risk profile.
  • Credit Analyst Agent. This agent is responsible for calculating the average credit score for the customer using the individual credit scores from the three main credit bureaus in the US. Tools used: Access to the credit scores from the three main credit bureaus in the US.
  • Loan Specialist Agent. This agent has the knowledge of the Bank’s loan approval policies and is responsible for making a loan approval or deny decisions based on both the customer risk profile and the average credit score. Knowledge used: Bank’s loan approval policy or rules.
  • Customer Communication Agent. This agent is an expert in customer communications and its role is to present the loan approval or denial decision to the customer. If the loan is denied, it will provide alternative personal loan options based on the current loan offerings. Knowledge used: Bank’s personal loan offerings.

This workflow executes in a semi-sequential fashion depending on the agent data interdependence. The Risk Analyst and Credit Analyst agents perform tasks simultaneously. The Loan Specialist agent depends on the output data from the previous two agents. Finally, the Customer Communication agent depends on the output data from the Loan Specialist agent.

During the design phase, a hierarchical workflow was considered, featuring a top-level “Manager Agent” responsible for assigning tasks and validating the output quality from other agents. However, at the time of implementation, CrewAI provided limited support for hierarchical workflows, leading to the adoption of this alternative approach.

Agentic workflow architecture

The following figure shows the workflow architecture. At a high level, there is a front-end managing the user experience, in the middle is the agentic workflow that represents the business logic, and then the back-end that provides data integration with cloud services. The overall platform is Red Hat OpenShift.

Architecture diagram of agentic workflow

Let’s look at each layer:

  • Back-end implementation. This layer represents the implementation of the customer risk and credit score APIs. There is a MongoDB container for storing the data (requires podman), a simple lightweight Python Flask implementation of the two back-end APIs, and then some sample data (name, SSN, credit score, risk assessment) for 10 bank customers.
  • Agentic workflow implementation. The agentic workflow framework used is CrewAI and is exposed as a Python Flask API to the front-end layer. This layer has access to the bank’s loan approval policies and personal loan offerings. Also, this layer is supported by IBM Granite large language models (LLMs) served by watsonx.ai, or OpenShift AI.
  • Front-end implementation. This layer has a simple web application that leverages React for the web client-side and Express (Node.js) for the web server-side. The web application allows users to register, login, submit a personal loan request, look a previous loan requests, and logoff. The user registration data is stored in the same MongoDB used by the back-end.

CrewAI implementation

CrewAI is an open-source Python agentic framework that enables developers to build and orchestrate collaborative teams of autonomous, role-based AI agents, each with defined roles, goals, backstories, and toolsets, that can communicate, delegate, and coordinate tasks among themselves, using modular components like Crews for organizing agent teams, and Processes for managing multi-agent interactions.

At a high level, defining an agentic workflow using the CrewAI framework requires the definition of Agents, Tasks, Tools, and a Crew where all these elements come together. Furthermore, some agents have access to tools as well as have domain-specific Knowledge.

Agents

Within the CrewAI ecosystem, an agent functions as a self-governing entity defined by three core attributes: a designated role, a driving goal, and a contextual backstory. These agents are empowered to execute specialized assignments, apply judgment aligned with their purpose and function, leverage various tools to meet their targets, collaborate with fellow agents, and when permitted, assign responsibilities to others. Learn more about agents in the CrewAI docs.

This is how agents are defined (agents.yaml):

risk_analyst:
  role: >
    Customer Risk Analyst
  goal: >
    Get the risk level of a customer requesting a loan using the social security number {ssn} provided
  backstory: >
    You are adept at getting the risk level of a customer requesting a loan by leveraging different tools 
    Your skill in identifying the customer risk level predicts the likelihood of loan repayment.
  allow_delegation: false
  verbose: true


credit_analyst:
  role: >
    Customer Credit Analyst
  goal: >
    Get the credit score of a customer requesting the loan using the social security number {ssn} provided
  backstory: >
    You are adept at getting the credit score of a customer requesting a loan by leveraging different tools 
    Your skill in identifying the customer credit score predicts the likelihood of loan repayment.
  allow_delegation: false
  verbose: true  


loan_specialist:
  role: >
    Customer Loan Specialist
  goal: >
    Combines the customer risk level and credit score to make a loan approval or rejection decision
  backstory: >
    You are an expert in making loan approval decisions and providing the reasons for loan approval or rejection.
  allow_delegation: false
  verbose: true 


customer_communications_specialist:
  role: >
    Customer Communications Specialist
  goal: >
    Communicate loan results. If loan was rejected, then find the best alternative loan options for the customer.
  backstory: >
    You are a seasoned banking professional with deep knowledge of the bank loan products that help customers secure financing.
  allow_delegation: false
  verbose: true

Tasks

A task within CrewAI represents a specific assignment delegated to an agent. Each task contains comprehensive details required for successful execution, including name identifier, description explaining the objective and the expected output criteria that define completion standards.

Tasks form the fundamental building blocks of agent. Each task bundles all essential information—from detailed instructions to tool requirements—giving agents everything needed to accomplish their objectives effectively. Learn more about tasks in the CrewAI docs.

This is how tasks are defined (tasks.yaml):

risk_analysis_task:
  description: >
    Use the provided tool to get a customer risk level using the customer social security number {ssn} and customer name {name}.
  expected_output: >
    A risk level value which can be low, medium or high. 


credit_analysis_task:
  description: >
    Use the provided tool to get a customer credit score using the customer social security number {ssn}.
  expected_output: >
    A credit score which is a number between 300 and 850.


loan_approval_task:
  description: >
    Evaluate both data elements, the customer risk level and credit score to make a loan approval or rejection decision
    Ensure that the customer meet the bank standards for loan approval in order to minimize the 
    likelihood of loan default. Make sure to review the decision statement to make sure is clear and consistent.
  expected_output: >
    A loan approval or rejection decision includind the reasons for the decision.


customer_communications_task:
  description: >
    Summarize loan approval decision into a customer friendly cordial response in bullet point format.
    Do not offer alternative loan options if the loan was approved.
    If the loan is rejected, offer alternative loan options.
    Only show loan options that require a cosigner for high risk customers.
    Review your response carefully and make sure you comply with these rules. 
  expected_output: >
    A well-structured response explaining the loan approval decision or if the loan was rejected, offer alternative loan options.

Tools

Within the CrewAI framework, agents derive their functional abilities from tools, which are specialized capabilities that enable a spectrum of operations from web research and data processing to cooperative interactions. These functional extensions serve as implementable skills that agents can deploy to accomplish diverse tasks. The toolset encompasses native options from the CrewAI Toolkit, integrated LangChain utilities, or purpose-built custom implementations developed for specific needs. Learn more about tasks in the CrewAI docs.

In this implementation, there are two custom tools that allow agents to obtain a risk assessment and credit score for a given bank customer. The tool implementation involves calling the customer risk and credit score APIs.

This is the Python custom tool implementation for the GetCreditScoreTool (custom_tool.py):

class GetCreditScoreToolInput(BaseModel):
    """Input schema for GetCreditScoreTool."""
    ssn: str = Field(..., description="social security number in the format NNN-NN-NNNN where N is a number")

class GetCreditScoreTool(BaseTool):
    name: str = "get credit score tool"
    description: str = "This tool is used for fetching the credit score for a given social security number. The parameter ssn represents the social security number for which to retrieve the credit score"
    args_schema: Type[BaseModel] = GetCreditScoreToolInput
    def _run(self, ssn: str) -> str:
        if is_valid_ssn(ssn):
            url = f"http://localhost:{PORT}/credit/{ssn}"
            auth_credentials = HTTPBasicAuth(USER, PASSW)  # Basic authentication
            try:
                response = requests.get(url, auth=auth_credentials)
                if response.status_code == 200:
                    return(response.json()["data"]["credit_score"])
                elif response.status_code == 404:
                    return("the social security number provided has not been found")
                else:
                    print("GetCreditScoreTool Failed:", response.status_code, response.text)
                    return("GetCreditScoreTool Failed, please try again")
            except requests.RequestException as e:
                print("Error:", e)
                return("GetCreditScoreTool Failed with Exception, please try again")
        else:
            return "the input parameter is an invalid social security number, please try again using the format NNN-NN-NNNN where N is a number"

Knowledge

Knowledge in CrewAI allows AI agents to access and use external information sources during their tasks. Agents can refer to it as a library of information while working, ensuring their responses are based on accurate and up-to-date facts. CrewAI supports various formats of knowledge sources including text, PDF, CSV, JSON, and so on. Learn more about knowledge in the CrewAI docs.

In this implementation, the Loan Specialist agent has the knowledge of the bank’s personal loan approval policy or rules. This is represented in the text file below:

*** Bank Loan Approval Rules ***

In order to minimize the likelihood of loan default, the bank will not approve 
any loan with a risk level value of "high", regardless of the credit score.

The bank will approve loans with risk level value of "medium" only when 
the credit score is above 700. Do not use additional conditions to approve the loan.

The bank will approve loans with risk level value of "low" only when 
the credit score is above 600. Do not use additional conditions to approve the loan.

The Customer Communications Specialist agent has the knowledge of alternative loan options based on the personal loan offerings that the bank publishes on PDF documents as shown in the following figure.

PDF documents for personal loan offerings

The Crew

Now, we have all the components needed to define the crew of agents. This is done in the crew.py file. Learn more about crews in the CrewAI docs.

As this is the core definition of the agentic workflow, this file is described in multiple steps.

The LLM section defines access the open source Granite models, available via watsonx.ai or OpenShift AI. The models used are granite-3-2-8b-instruct and granite-embedding-107m-multilingual. The following code shows the LLM sections:

watson_llm = LLM(
    model="watsonx/ibm/granite-3-2-8b-instruct",         
    base_url=watsonx_url,
    api_key=watsonx_api_key,
    project_id=watsonx_project_id,
    max_tokens=1000,
    temperature=0.1, 
)

watson_embedding={
    "provider": "watson",
    "config": {
        "model": "ibm/granite-embedding-107m-multilingual",
        "api_url": watsonx_url,
        "api_key": watsonx_api_key,
        "project_id": watsonx_project_id,               
    }
}

rhoai_llm = LLM(
    model="openai/granite",
    base_url=rhoai_url,
    api_key=rhoai_api_key,
    max_tokens=1000,
    temperature=0.1,
)

rhoai_embedding = {
    "provider": "openai",
    "config": {
        "model": "granite-embedding",
        "api_base": rhoai_embed_url,
        "api_key": rhoai_api_key,
    }
}

The knowledge section defines access to the loan approval rules and the loan product list.

# Knowledge sources for loan approval
loan_approval_knowledge = TextFileKnowledgeSource(
    file_paths=["loan_approval_knowledge.txt"],
    chunk_size=4000,      # Maximum size of each chunk (default: 4000)
    chunk_overlap=200,     # Overlap between chunks (default: 200)
)

# Knowledge sources for alternative loan approval
alternative_loan_knowledge = PDFKnowledgeSource(
    file_paths=["loan-products.pdf"],   
    chunk_size=4000,      # Maximum size of each chunk (default: 4000)
    chunk_overlap=200,     # Overlap between chunks (default: 200)

The task data format section defines the tasks data output format using Pydantic model data structures.

 class Risk(BaseModel):
    risk_level: Literal['low', 'medium', 'high'] = Field(..., description="Risk level must be 'low', 'medium', or 'high'.")

class CreditScore(BaseModel):
    credit_score: int = Field(..., ge=300, le=850, description="The credit score of the customer requesting a loan (300-850).")

class LoanDecision(BaseModel):
    response: str = Field(..., description="The loan application decision.")

class LoanResponse(BaseModel):
    response: str = Field(..., description="The customer friendly response to the loan application.")

The crew definition is done using the @CrewBase decorator. Each agent gets assigned the role, goal, and backstory from the yaml file, the LLMs, and knowledge sources.

@CrewBase
class FinProject():
    """FinProject crew"""

    agents_config = 'config/agents.yaml'
    tasks_config = 'config/tasks.yaml'

    @agent
    def risk_analyst(self) -> Agent:
        return Agent(
            config=self.agents_config['risk_analyst'],
            llm=watson_llm,         
        )

    @agent
    def credit_analyst(self) -> Agent:
        return Agent(
            config=self.agents_config['credit_analyst'],
            llm=watson_llm,                 
        )

    @agent
    def loan_specialist(self) -> Agent:
        return Agent(
            config=self.agents_config['loan_specialist'],
            llm=watson_llm,
            knowledge_sources=[loan_approval_knowledge],
            embedder=watson_embedding,          
        )

    @agent
    def customer_communications_specialist(self) -> Agent:
        return Agent(
            config=self.agents_config['customer_communications_specialist'],
            llm=watson_llm,
            knowledge_sources=[alternative_loan_knowledge],
            embedder=watson_embedding,          
        )

Each task gets assigned the description and expected output from the yaml file, which agent can run the task, the tools available, the data output format and the execution model. Note that the first two tasks don’t have any dependencies and are therefore defined as asynchronous. Note that the context in the loan approval task reference the first two tasks as dependencies.

@task
    def risk_analysis_task(self) -> Task:
        return Task(
            config=self.tasks_config['risk_analysis_task'],
            agent=self.risk_analyst(),
            tools=[GetRiskAssessmentTool()],        
            output_pydantic=Risk,
            async_execution=True,
        )

    @task
    def credit_analysis_task(self) -> Task:
        return Task(
            config=self.tasks_config['credit_analysis_task'],
            agent=self.credit_analyst(),
            tools=[GetCreditScoreTool()],
            output_pydantic=CreditScore,
            async_execution=True,
        )

    @task
    def loan_approval_task(self) -> Task:
        return Task(
            config=self.tasks_config['loan_approval_task'],
            agent=self.loan_specialist(),
            context=[self.risk_analysis_task(), self.credit_analysis_task()],   
            output_json=LoanDecision,
        )

    @task
    def customer_communications_task(self) -> Task:
        return Task(
            config=self.tasks_config['customer_communications_task'],
            agent=self.customer_communications_specialist(),
            context=[self.loan_approval_task()],
            output_json=LoanResponse,
        )

Finally, the @crew decorator groups all the agents, the tasks, and the process implementation as sequential.

  @crew
    def crew(self) -> Crew:
        """Creates the FinProject crew"""

        return Crew(
            agents=self.agents,
            tasks=self.tasks,
            process=Process.sequential,
            cache=False,
            share_crew=False,
            verbose=True,

Sample Loan Requests

The following figure shows a $25K personal auto loan that has been approved by the agentic workflow because this customer has a low risk level and a high credit score.

Approval message from agentic workflow

The following figure shows a $38K personal auto loan that has been denied by the agentic workflow. Although the customer has a high credit score, the customer was flagged as high risk by the risk analyst agent. Despite the loan denial, the customer was offered two alternative loan options with higher interest rates that require a cosigner.

Denial message from agentic workflow

Conclusion

This article demonstrates the transformative potential of AI agentic workflows in financial decision-making use cases such as the personal loan origination process. By leveraging agents with access to domain-specific knowledge and tools for credit scoring and risk analysis, the proposed solution showcases how AI can help to enhance the efficiency and responsiveness of financial services.

The article also examines the CrewAI framework's adaptability in implementing agentic workflows without writing large amounts of code.

Hopefully this article inspires fresh perspectives on applying AI agents across the financial services landscape. As the field continues to evolve, early adopters who thoughtfully integrate agentic AI solutions may fundamentally transform how financial services are delivered, experienced, and valued in our increasingly digital economy.

Acknowledgments

This finance agentic workflow design and implementation was done in the context of the IBM Open Innovation Community (OIC) “Better Together Data & AI – AI Assistants” initiative. The following people contributed to this project: Jorge Gonzalez Orozco (IBM), Bruce Torres (IBM), Saurabh Agarwal (Red Hat), Melvin Hillsman (Red Hat), Kris Verlaenen (Red Hat), and Thalia Hooker (Red Hat).

Source code

The source code is published in this repo: https://github.com/IBM/i-oic-better-together-data-ai-assistants-finance. Note that this is PoC quality code not meant to be deployed as-is in a production environment. The source code has not been containerized. The code has been tested in a MacBook Pro laptop with macOS Sequoia 15.14 but it could be adapted to other platforms.

There is a separate folder for the back-end, front-end and agentic workflow. Each folder has a README.md file that describes in detail how to stand-up each of the layers. Make sure you carefully follow the instructions in each README.md file.

At a high level, these are the steps to run the solution:

  1. Stand up the back-end layer including deploying the MongoDB container, the back-end APIs and loading the sample bank customer data.
  2. Stand up the agentic workflow API.
  3. Stand up the front-end web application.