How to Build a Grant-Matching AI Agent For Your Research Project A developer published a step-by-step build of a grant-matching AI agent that researches funding opportunities against a research project's profile and returns a cited shortlist with eligibility questions and next actions. The workflow uses Valyu's DeepResearch engine for discovery, TypeScript via valyu-js 2.10.5 for inputs and result storage, and a scheduler for daily runs, illustrated with a UK university water-sensor project seeking GBP 100,000 to GBP 500,000. Day 15 of 30 Days of Search. A university research group wants to develop low-cost sensors for drinking-water contamination. It needs funding for prototype development, lab validation and a community pilot. A search for “water research grants” returns opportunities, but the research office still has to work out which accept a university lead, which cover the proposed work and which are still open. That investigation repeats every week. The project stays broadly the same; funding calls and deadlines change. We will build a grant-matching agent with an optimized workflow that researches opportunities against a project profile and produces a cited shortlist with eligibility questions and next actions. We will start with the TypeScript code, create its workflow in the Valyu platform, then connect the two. Finally, we will schedule daily runs and show the manual weekly route. valyu-js 2.10.5 and a Valyu API key A grant-matching workflow researches funding opportunities against an applicant's project, location, eligibility and funding requirements, then returns a shortlist with evidence. Its output helps a research office decide which opportunities deserve an application discussion. In this build, Valyu's DeepResearch engine https://docs.valyu.ai/guides/deepresearch handles discovery and research. The saved workflow defines the assignment. TypeScript supplies the changing inputs and stores the result. A scheduler decides when to invoke the code. The schedule belongs to the job runner. The research instructions belong to the saved Valyu workflow. Our sample profile is illustrative, not a representation of a particular university or researcher: | Requirement | Example | |---|---| | Project | Low-cost drinking-water contamination sensors, including laboratory validation and a community pilot | | Applicant | UK university group; proposed lead is a lecturer with three years of independent research experience | | Partnerships | No confirmed commercial lead or industry partner | | Funding | GBP 100,000 to GBP 500,000 over 12 to 24 months | | Search window | Open or officially announced upcoming calls with deadlines in the next 90 days; rolling schemes allowed | | Useful result | Up to five candidates with sources, conditions and next actions, or an explanation that no suitable match was verified | Install Node.js 24 or newer, then create a folder: mkdir grant-matching-workflow cd grant-matching-workflow npm init -y npm pkg set type=module npm install --save-exact valyu-js@2.10.5 Node can execute the erasable TypeScript used here directly. We do not need a separate build step to run this example. See Node's TypeScript support https://nodejs.org/api/typescript.html for the supported syntax. Create .gitignore : node modules/ .env results/ Get an API key from the Valyu platform https://platform.valyu.ai/user/account/apikeys and put it in a local .env file: VALYU API KEY=your valyu api key The key must belong to the organisation where you create the workflow. An otherwise valid key for another organisation cannot see that organisation's private workflow. Keep the key out of the article, screenshots and version control. Create project.json : { "project": "Develop and field-test low-cost drinking-water sensors that detect contamination in community water systems. The project needs prototype development, laboratory validation and a small field pilot with community partners.", "applicant": "A UK university research group. The proposed lead is a lecturer with three years of independent research experience. No commercial lead or confirmed industry partner. The university's funder registration and internal approvals must be checked.", "geography": "United Kingdom; international collaborations are possible where a UK university can lead.", "funding needs": "GBP 100,000 to GBP 500,000 over 12 to 24 months. Research and pilot funding, not equity investment, loans, student scholarships or travel-only awards. Find open calls and officially announced upcoming calls with a deadline in the next 90 days; rolling calls are acceptable." } The applicant description matters as much as the project description. “Water sensors” can match a call's subject while the call requires a business lead, an established consortium or a particular career stage. We keep the run date out of this file. The runner calculates a fresh UTC date each time, so tomorrow's scheduled job does not reuse today's cut-off. The --date option is useful when recording a walkthrough; it does not turn live research into a historical snapshot. Create run-grants.ts : js import { mkdir, open, readFile, writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { parseArgs } from 'node:util'; import { pathToFileURL } from 'node:url'; import { Valyu } from 'valyu-js'; export const WORKFLOW = 'grant-matching-brief'; export const VERSION = 1; export type Profile = Record<'project' | 'applicant' | 'geography' | 'funding needs', string ; export function parameters profile: Profile, date = new Date .toISOString .slice 0, 10 { for const key of 'project', 'applicant', 'geography', 'funding needs' as const { if typeof profile key == 'string' || profile key .trim throw new Error Missing profile field: ${key} ; } if /^\d{4}-\d{2}-\d{2}$/.test date || Number.isNaN Date.parse date || new Date date .toISOString .slice 0, 10 == date throw new Error 'Use a valid YYYY-MM-DD date.' ; return { project: profile.project, applicant: profile.applicant, geography: profile.geography, funding needs: profile.funding needs, as of date: date }; } export async function runGrants client: Valyu, profile: Profile, options: { execute?: boolean; date?: string; directory: string; resume?: string; } { await mkdir options.directory, { recursive: true } ; const taskPath = join options.directory, 'task.json' ; const resultPath = join options.directory, 'result.json' ; let taskId = options.resume; if taskId { const workflowParams = parameters profile, options.date ; const preview = await client.workflows.preview WORKFLOW, { workflowParams, workflowVersion: VERSION } ; if preview.success || preview.workflow?.slug == WORKFLOW || preview.workflow.version == VERSION || preview.resolved?.input { throw new Error 'Workflow preview failed. Check the slug, version, inputs and API-key organisation.' ; } await writeFile join options.directory, 'preview.json' , JSON.stringify preview, null, 2 ; if options.execute return { preview }; // An existing receipt prevents a second submission from this results directory. const receipt = await open taskPath, 'wx' ; try { await receipt.writeFile JSON.stringify { status: 'submitting', workflow: WORKFLOW, version: VERSION, workflowParams } ; await receipt.sync ; const task = await client.deepresearch.create { workflowId: WORKFLOW, workflowVersion: VERSION, workflowParams, mode: 'standard' } ; if task.success || task.deepresearch id throw new Error 'Task creation failed; inspect the platform before retrying.' ; taskId = task.deepresearch id; await receipt.truncate 0 ; await receipt.write JSON.stringify { taskId, workflow: task.workflow, workflowParams }, null, 2 , 0, 'utf8' ; await receipt.sync ; } finally { await receipt.close ; } } console.log Research task: ${taskId} ; console.log Resume: node run-grants.ts --resume ${taskId} --out ${JSON.stringify options.directory } ; const result = await client.deepresearch.wait taskId, { pollInterval: 10 000, maxWaitTime: 30 60 1000 } ; await writeFile resultPath, JSON.stringify result, null, 2 ; if result.success || result.status == 'completed' || typeof result.output == 'string' { throw new Error Report not complete. Inspect task ${taskId}; resume polling rather than submit another run. ; } await writeFile join options.directory, 'report.md' , result.output ; return { taskId, result }; } async function main { const { values } = parseArgs { options: { run: { type: 'boolean', default: false }, profile: { type: 'string', default: 'project.json' }, date: { type: 'string' }, out: { type: 'string' }, resume: { type: 'string' }, } } ; if process.env.VALYU API KEY throw new Error 'Set VALYU API KEY for the organisation that owns the workflow.' ; const directory = resolve values.out ?? join 'results', new Date .toISOString .replaceAll ':', '-' ; const profile = values.resume ? {} as Profile : JSON.parse await readFile values.profile , 'utf8' as Profile; const output = await runGrants new Valyu process.env.VALYU API KEY , profile, { execute: values.run, date: values.date, directory, resume: values.resume, } ; console.log 'result' in output ? Saved ${join directory, 'report.md' } : Preview saved in ${directory}. Add --run to start research. ; } if process.argv 1 && import.meta.url === pathToFileURL resolve process.argv 1 .href { main .catch = { console.error 'Grant workflow failed. Inspect results/task.json and the platform task before retrying. Check the key, profile and workflow version.' ; process.exitCode = 1; } ; } There are three important API calls: workflows.preview deepresearch.create deepresearch.wait The create request uses workflowId , workflowVersion and workflowParams . The research prompt stays in the saved workflow. Do not also pass a freeform query , input , researchStrategy or reportFormat ; those fields are mutually exclusive with a workflow run. The runner writes a task receipt before waiting. If the local process stops, use the recorded task ID with --resume . If a receipt only says submitting , check the platform task records before trying another submission: the request may have reached the service even if the client did not receive its ID. The local receipt prevents another invocation from submitting into the same results directory; it is not a distributed exactly-once guarantee. Prepare the files now; run them after the next step. The workflow slug does not exist in your organisation until you create it. Open Workflows in the Valyu platform https://platform.valyu.ai/user/workflows and click New workflow . Under Identity , enter: | Field | Value | |---|---| | Title | Grant Matching Brief | | Description | Find research funding opportunities, check eligibility and deadlines against a project profile, and return a cited shortlist with next actions. | Check the generated slug: grant-matching-brief . It must match WORKFLOW in the TypeScript file. Your organisation owns this custom template; readers following the tutorial create their own copy. The template has three fields. Paste these instructions into the corresponding fields. As of {as of date}, find research grants that could fund this project: {project}. Applicant profile: {applicant}. Location and collaboration constraints: {geography}. Funding requirements: {funding needs}. Return up to five credible opportunities, not a target you must fill. Verify each call's current status, applicant eligibility, scope, award size, duration and deadline from official funder or application pages. Distinguish an open call from an officially announced upcoming call, a rolling scheme and an expired previous round. Keep uncertain eligibility visible; a topic match alone does not establish eligibility. Do not invent open calls, dates, award amounts or funding programmes. If no suitable opportunity is verified, say so and explain the gaps. Search official funder websites, programme pages, call documents and application portals. Use directories and news only to discover candidates, then check the official call. For each candidate, verify the named call and round, lead-applicant country and organisation rules, career-stage restrictions, required partners, project scope, amount and currency, eligible costs, duration, deadline including time zone if published, and current open/upcoming/rolling/closed status. Do not infer a new round from last year's dates. Distinguish an expression-of-interest deadline from the full application deadline. If an official page is inaccessible or facts conflict, record what could not be verified and mark the candidate needs verification. Deduplicate by funder, call and round, not by every page describing it. Prioritising official sources here is a research instruction, not a request-level domain allowlist. Produce a concise Markdown grant-matching brief. Start with the run date, project profile and a short decision summary. Include a shortlist table with programme and round, funder, status, amount/currency, duration, deadline/time zone or rolling, fit classification potential match, conditional match, not eligible, needs verification , decisive eligibility evidence, outstanding requirement and next action. Add a compact evidence section for every shortlisted opportunity: link the official call and eligibility/deadline sources, state why it fits and the exact unresolved conditions. Separate ruled-out candidates with reasons. Finish with actions for the research office and search coverage/gaps. Do not describe a conditional match as confirmed eligible. Cite factual claims, preserve source URLs and use unknown where the source does not establish a value. A shortlist helps decide what to investigate; do not imply that an application was submitted or funding was awarded. The project changes between runs. The instructions for investigating calls and explaining eligibility stay in the template. The prompt requests up to five credible opportunities. Five is a maximum, not a requirement to pad the report with weak candidates. A useful result may be “no verified full match,” followed by the closest conditional routes and the conditions that need checking. The platform detects the {variable} placeholders. Configure the five rows as follows and keep them required: | Key | Type | Purpose | |---|---|---| | as of date | date | The date used for the current investigation | | project | textarea | What the project will do and deliver | | applicant | textarea | Organisation type, career stage and known eligibility facts | | geography | text | Lead-applicant location and collaboration constraints | | funding needs | textarea | Amount, duration, funding type and deadline window | Use the exact keys from the code. funding needs and fundingNeeds are different inputs. Choose: Initial grant matching template with official call verification and conditional eligibility. Click Create workflow . Confirm that the saved row shows Grant Matching Brief , grant-matching-brief , v1 and STANDARD . Saving creates the reusable template. It does not start a funding search. The “official sources first” strategy is an instruction to the research engine, not an enforced domain allowlist. If your application requires a hard source restriction, configure the run's search.includedSources using the DeepResearch search options https://docs.valyu.ai/guides/deepresearch . This example leaves discovery broad so it can find funders we have not named in advance. From the project folder, run: node --env-file=.env run-grants.ts --out results/preview This saves results/preview/preview.json . Check that it contains: { "workflow": { "slug": "grant-matching-brief", "version": 1 } } The complete response also includes resolved.input , resolved.research strategy , resolved.report format , mode and tools. Open the file and confirm that your project, applicant and run date have replaced the placeholders. If preview fails, check the organisation associated with the key, the slug, the version and the five input names. Fix that before starting research. Start a research task: node --env-file=.env run-grants.ts --run --out results/first-scan The runner prints the task ID and a resume command. When research completes, the directory contains: results/first-scan/ preview.json task.json result.json report.md report.md is the readable briefing. result.json preserves the API response, and task.json records the task ID, workflow version and submitted inputs. If waiting is interrupted, read the task ID from task.json and resume polling: node --env-file=.env run-grants.ts \ --resume YOUR EXISTING TASK ID \ --out results/first-scan This path polls the existing task rather than creating another one. A local polling timeout does not mean the remote investigation was cancelled. I ran the saved version 1 through this TypeScript runner with the illustrative profile and --date 2026-10-15 . The task completed with a cited Markdown report. Its main conclusion was: No opportunity is a verified clean match. That is the research result for this run, not proof that no suitable funding exists. The report named conditional candidates, ruled out others and described gaps in the search. Two findings show why the applicant profile is essential: | Candidate from the run | Evidence checked for this article | Useful next action | |---|---|---| | EPSRC New Investigator Award: applicant-led mode | The official UKRI page https://www.ukri.org/opportunity/epsrc-new-investigator-award-applicant-led-mode/ lists a rolling scheme and eligibility conditions based on prior applications, funding history, supervision and career independence. A lecturer's job title alone does not settle eligibility. | Ask the research office to check the proposed lead's record and the scheme's aims before preparing an application. | | Water Breakthrough Challenge 7, Catalyst Stream | The official competition page https://waterinnovation.challenges.org/breakthrough7/ requires an appointed water company in England or Wales to lead. Other organisations can participate as partners. | Treat this as a possible partnership route, not a university-led application. | The review also caught a concrete error: the generated report called 5 January 2027 outside the 90-day window. It is inside the window ending 13 January 2027 . The water-company lead restriction still rules out our sample university as lead, but the date label needed correcting. Check extracted dates with calendar arithmetic before using them to filter or prioritise calls. The raw report retained secondary-source citations and some unverified fields. Open the official call pages for candidates you want to pursue. Keep those corrections with the saved report rather than silently treating generated classifications as established facts. Research should turn a candidate into a decision the team can inspect: pursue, resolve a condition, consider a partner role or rule it out. A colleague can use the same saved workflow without running TypeScript: as of date . These are the same five inputs sent by the TypeScript runner. Use workflow prepares the request. The separate Run button starts the research task. For a weekly research-office meeting, run it before the meeting and bring the shortlist. Change the applicant details when the project gains a partner, the funding target changes or the research office resolves an eligibility question. This is a manual weekly routine. The code schedule in the next step is how we automate the frequency. The runner executes once and exits. Put it behind a scheduler to call it at the frequency you want. We will use GitHub Actions so the computer running this tutorial does not need to stay on. Put run-grants.ts , project.json , package.json and package-lock.json at the root of a repository. Add VALYU API KEY as a repository Actions secret under Settings → Secrets and variables → Actions . If the project stays in a subfolder, set the job's working directory and npm cache dependency path to that folder. Create .github/workflows/grant-scan.yml : name: Grant funding scan on: workflow dispatch: schedule: - cron: '17 8 ' permissions: contents: read concurrency: group: grant-matching-brief cancel-in-progress: false jobs: research: runs-on: ubuntu-latest timeout-minutes: 40 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: '24' cache: npm - run: npm ci - name: Run the saved Valyu workflow env: VALYU API KEY: ${{ secrets.VALYU API KEY }} run: node run-grants.ts --run --out results/${{ github.run id }}-${{ github.run attempt }} - name: Keep reports and task receipts if: always uses: actions/upload-artifact@v7 with: name: grant-brief-${{ github.run id }}-${{ github.run attempt }} path: results/ if-no-files-found: ignore retention-days: 30 This starts the grant scan at 08:17 UTC each day . The runner computes that day's as of date automatically. Reports and task receipts are retained as Actions artifacts, including when a run fails after submitting a task. Change only the cron expression to change the frequency: | Frequency | Cron expression | Meaning in this example | |---|---|---| | Daily | 17 8 | Every day at 08:17 UTC | | Weekdays | 17 8 1-5 | Monday to Friday at 08:17 UTC | | Weekly | 17 8 1 | Every Monday at 08:17 UTC | | Monthly | 17 8 1 | The first day of each month at 08:17 UTC | Keep one of these schedule entries for the cadence you want. workflow dispatch also adds a manual Run workflow button in GitHub Actions. GitHub schedules run from the default branch and can be delayed. This example uses the default UTC scheduling behaviour; choose a timezone explicitly if you need local wall-clock timing. See GitHub's schedule reference https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows schedule for the current rules and supported intervals. The concurrency group prevents overlapping Actions jobs. It does not cancel a remote Valyu task if a runner disappears. Before rerunning a failed job, inspect its receipt and resume the original task when possible. A new Actions attempt has a new results directory and would otherwise start a fresh research task. Start with a weekly schedule if that matches your team's decision cycle. Every execution starts fresh research and uses credits, even when no new grant matches the project. For every candidate worth pursuing, the research office needs to establish: Keep each saved report with its date and workflow version. A later run can bring new calls or changed deadlines. This runner saves separate snapshots; it does not automatically detect new grants or compare reports. For a changes-only digest, add a comparison step keyed by funder, call identifier and round. If you improve the research instructions, save a new workflow version with a changelog. Test the revised template, then deliberately update VERSION in the runner. Pinning a version keeps the brief stable while new research uses current sources. Yes. Save a grant-research workflow with project and applicant inputs, call it through deepresearch.create and schedule the runner. The result is an asynchronous research task whose report you can save and review. Yes. Open the saved workflow, fill its inputs and run it manually from the playground. The TypeScript runner adds repeatable invocation, local files and external scheduling. It researches published eligibility conditions and identifies what the supplied profile does or does not establish. Missing applicant information remains unresolved. The funder's rules and the research office's checks determine whether an application can proceed. Not for this build. The runner calls Valyu's DeepResearch service, which performs the research. It uses one Valyu API key belonging to the workflow's organisation. No. Each run researches the current profile and saves a new report. A new-grants-only alert needs stored history and a comparison step; a fresh run date by itself does not provide deduplication across runs. Create the profile, save Grant Matching Brief , preview it and run one search. Review the shortlist with the people who will prepare the application. Once its questions and output fit that process, choose a schedule that matches when the team makes funding decisions. The useful output is a source-backed answer to “Which opportunities should we investigate next, and what must we confirm before applying?”