feat(platform): Add instructions field to agent submissions (#10931)

## Summary

Added an optional "Instructions" field for agent submissions to help
users understand how to run agents and what to expect.

<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/015c4f0b-4bdd-48df-af30-9e52ad283e8b"
/>

<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/3242cee8-a4ad-4536-bc12-64b491a8ef68"
/>

<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/a9b63e1c-94c0-41a4-a44f-b9f98e446793"
/>


### Changes Made

**Backend:**
- Added `instructions` field to `AgentGraph` and `StoreListingVersion`
database models
- Updated `StoreSubmission`, `LibraryAgent`, and related Pydantic models
- Modified store submission API routes to handle instructions parameter
- Updated all database functions to properly save/retrieve instructions
field
- Added graceful handling for cases where database doesn't yet have the
field

**Frontend:**
- Added instructions field to agent submission flow (PublishAgentModal)
- Positioned below "Recommended Schedule" section as specified
- Added instructions display in library/run flow (RunAgentModal)  
- Positioned above credentials section with informative blue styling
- Added proper form validation with 2000 character limit
- Updated all TypeScript types and API client interfaces

### Key Features

-  Optional field - fully backward compatible
-  Proper positioning in both submission and run flows
-  Character limit validation (2000 chars)
-  User-friendly display with "How to use this agent" styling
-  Only shows when instructions are provided

### Testing

- Verified Pydantic model validation works correctly
- Confirmed schema validation enforces character limits
- Tested graceful handling of missing database fields
- Code formatting and linting completed

## Test plan

- [ ] Test agent submission with instructions field
- [ ] Test agent submission without instructions (backward
compatibility)
- [ ] Verify instructions display correctly in run modal
- [ ] Test character limit validation
- [ ] Verify database migrations work properly

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Zamil Majdy
2025-09-17 10:55:45 +07:00
committed by GitHub
parent fc8c5ccbb6
commit 33679f3ffe
21 changed files with 215 additions and 1 deletions

View File

@@ -161,6 +161,7 @@ class BaseGraph(BaseDbModel):
is_active: bool = True
name: str
description: str
instructions: str | None = None
recommended_schedule_cron: str | None = None
nodes: list[Node] = []
links: list[Link] = []
@@ -705,6 +706,7 @@ class GraphModel(Graph):
is_active=graph.isActive,
name=graph.name or "",
description=graph.description or "",
instructions=graph.instructions,
recommended_schedule_cron=graph.recommendedScheduleCron,
nodes=[NodeModel.from_db(node, for_export) for node in graph.Nodes or []],
links=list(

View File

@@ -43,6 +43,7 @@ class LibraryAgent(pydantic.BaseModel):
name: str
description: str
instructions: str | None = None
input_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
output_schema: dict[str, Any]
@@ -126,6 +127,7 @@ class LibraryAgent(pydantic.BaseModel):
updated_at=updated_at,
name=graph.name,
description=graph.description,
instructions=graph.instructions,
input_schema=graph.input_schema,
output_schema=graph.output_schema,
credentials_input_schema=(

View File

@@ -499,6 +499,7 @@ async def get_store_submissions(
sub_heading=sub.sub_heading,
slug=sub.slug,
description=sub.description,
instructions=getattr(sub, "instructions", None),
image_urls=sub.image_urls or [],
date_submitted=sub.date_submitted or datetime.now(tz=timezone.utc),
status=sub.status,
@@ -590,6 +591,7 @@ async def create_store_submission(
video_url: str | None = None,
image_urls: list[str] = [],
description: str = "",
instructions: str | None = None,
sub_heading: str = "",
categories: list[str] = [],
changes_summary: str | None = "Initial Submission",
@@ -661,6 +663,7 @@ async def create_store_submission(
video_url=video_url,
image_urls=image_urls,
description=description,
instructions=instructions,
sub_heading=sub_heading,
categories=categories,
changes_summary=changes_summary,
@@ -682,6 +685,7 @@ async def create_store_submission(
videoUrl=video_url,
imageUrls=image_urls,
description=description,
instructions=instructions,
categories=categories,
subHeading=sub_heading,
submissionStatus=prisma.enums.SubmissionStatus.PENDING,
@@ -712,6 +716,7 @@ async def create_store_submission(
slug=slug,
sub_heading=sub_heading,
description=description,
instructions=instructions,
image_urls=image_urls,
date_submitted=listing.createdAt,
status=prisma.enums.SubmissionStatus.PENDING,
@@ -744,6 +749,7 @@ async def edit_store_submission(
categories: list[str] = [],
changes_summary: str | None = "Update submission",
recommended_schedule_cron: str | None = None,
instructions: str | None = None,
) -> backend.server.v2.store.model.StoreSubmission:
"""
Edit an existing store listing submission.
@@ -824,6 +830,7 @@ async def edit_store_submission(
categories=categories,
changes_summary=changes_summary,
recommended_schedule_cron=recommended_schedule_cron,
instructions=instructions,
)
# For PENDING submissions, we can update the existing version
@@ -840,6 +847,7 @@ async def edit_store_submission(
subHeading=sub_heading,
changesSummary=changes_summary,
recommendedScheduleCron=recommended_schedule_cron,
instructions=instructions,
),
)
@@ -858,6 +866,7 @@ async def edit_store_submission(
sub_heading=sub_heading,
slug=current_version.StoreListing.slug,
description=description,
instructions=instructions,
image_urls=image_urls,
date_submitted=updated_version.submittedAt or updated_version.createdAt,
status=updated_version.submissionStatus,
@@ -899,6 +908,7 @@ async def create_store_version(
video_url: str | None = None,
image_urls: list[str] = [],
description: str = "",
instructions: str | None = None,
sub_heading: str = "",
categories: list[str] = [],
changes_summary: str | None = "Initial submission",
@@ -967,6 +977,7 @@ async def create_store_version(
videoUrl=video_url,
imageUrls=image_urls,
description=description,
instructions=instructions,
categories=categories,
subHeading=sub_heading,
submissionStatus=prisma.enums.SubmissionStatus.PENDING,
@@ -988,6 +999,7 @@ async def create_store_version(
slug=listing.slug,
sub_heading=sub_heading,
description=description,
instructions=instructions,
image_urls=image_urls,
date_submitted=datetime.now(),
status=prisma.enums.SubmissionStatus.PENDING,
@@ -1415,6 +1427,7 @@ async def review_store_submission(
"name": store_listing_version.name,
"description": store_listing_version.description,
"recommendedScheduleCron": store_listing_version.recommendedScheduleCron,
"instructions": store_listing_version.instructions,
},
)
@@ -1580,6 +1593,7 @@ async def review_store_submission(
else ""
),
description=submission.description,
instructions=submission.instructions,
image_urls=submission.imageUrls or [],
date_submitted=submission.submittedAt or submission.createdAt,
status=submission.submissionStatus,
@@ -1715,6 +1729,7 @@ async def get_admin_listings_with_versions(
sub_heading=version.subHeading,
slug=listing.slug,
description=version.description,
instructions=version.instructions,
image_urls=version.imageUrls or [],
date_submitted=version.submittedAt or version.createdAt,
status=version.submissionStatus,

View File

@@ -49,6 +49,7 @@ class StoreAgentDetails(pydantic.BaseModel):
creator_avatar: str
sub_heading: str
description: str
instructions: str | None = None
categories: list[str]
runs: int
rating: float
@@ -103,6 +104,7 @@ class StoreSubmission(pydantic.BaseModel):
sub_heading: str
slug: str
description: str
instructions: str | None = None
image_urls: list[str]
date_submitted: datetime.datetime
status: prisma.enums.SubmissionStatus
@@ -157,6 +159,7 @@ class StoreSubmissionRequest(pydantic.BaseModel):
video_url: str | None = None
image_urls: list[str] = []
description: str = ""
instructions: str | None = None
categories: list[str] = []
changes_summary: str | None = None
recommended_schedule_cron: str | None = None
@@ -168,6 +171,7 @@ class StoreSubmissionEditRequest(pydantic.BaseModel):
video_url: str | None = None
image_urls: list[str] = []
description: str = ""
instructions: str | None = None
categories: list[str] = []
changes_summary: str | None = None
recommended_schedule_cron: str | None = None

View File

@@ -532,6 +532,7 @@ async def create_submission(
video_url=submission_request.video_url,
image_urls=submission_request.image_urls,
description=submission_request.description,
instructions=submission_request.instructions,
sub_heading=submission_request.sub_heading,
categories=submission_request.categories,
changes_summary=submission_request.changes_summary or "Initial Submission",
@@ -578,6 +579,7 @@ async def edit_submission(
video_url=submission_request.video_url,
image_urls=submission_request.image_urls,
description=submission_request.description,
instructions=submission_request.instructions,
sub_heading=submission_request.sub_heading,
categories=submission_request.categories,
changes_summary=submission_request.changes_summary,

View File

@@ -0,0 +1,53 @@
-- Add instructions field to AgentGraph and StoreListingVersion tables and update StoreSubmission view
BEGIN;
-- AddColumn
ALTER TABLE "AgentGraph" ADD COLUMN "instructions" TEXT;
-- AddColumn
ALTER TABLE "StoreListingVersion" ADD COLUMN "instructions" TEXT;
-- Drop the existing view
DROP VIEW IF EXISTS "StoreSubmission";
-- Recreate the view with the new instructions field
CREATE VIEW "StoreSubmission" AS
SELECT
sl.id AS listing_id,
sl."owningUserId" AS user_id,
slv."agentGraphId" AS agent_id,
slv.version AS agent_version,
sl.slug,
COALESCE(slv.name, '') AS name,
slv."subHeading" AS sub_heading,
slv.description,
slv.instructions,
slv."imageUrls" AS image_urls,
slv."submittedAt" AS date_submitted,
slv."submissionStatus" AS status,
COALESCE(ar.run_count, 0::bigint) AS runs,
COALESCE(avg(sr.score::numeric), 0.0)::double precision AS rating,
slv.id AS store_listing_version_id,
slv."reviewerId" AS reviewer_id,
slv."reviewComments" AS review_comments,
slv."internalComments" AS internal_comments,
slv."reviewedAt" AS reviewed_at,
slv."changesSummary" AS changes_summary,
slv."videoUrl" AS video_url,
slv.categories
FROM "StoreListing" sl
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
LEFT JOIN (
SELECT "AgentGraphExecution"."agentGraphId", count(*) AS run_count
FROM "AgentGraphExecution"
GROUP BY "AgentGraphExecution"."agentGraphId"
) ar ON ar."agentGraphId" = slv."agentGraphId"
WHERE sl."isDeleted" = false
GROUP BY sl.id, sl."owningUserId", slv.id, slv."agentGraphId", slv.version, sl.slug, slv.name,
slv."subHeading", slv.description, slv.instructions, slv."imageUrls", slv."submittedAt",
slv."submissionStatus", slv."reviewerId", slv."reviewComments", slv."internalComments",
slv."reviewedAt", slv."changesSummary", slv."videoUrl", slv.categories, ar.run_count;
COMMIT;

View File

@@ -110,6 +110,7 @@ model AgentGraph {
name String?
description String?
instructions String?
recommendedScheduleCron String?
isActive Boolean @default(true)
@@ -757,6 +758,7 @@ model StoreListingVersion {
videoUrl String?
imageUrls String[]
description String
instructions String?
categories String[]
isFeatured Boolean @default(false)

View File

@@ -11,6 +11,7 @@
"creator_avatar": "avatar1.jpg",
"sub_heading": "Test agent subheading",
"description": "Test agent description",
"instructions": null,
"categories": [
"category1",
"category2"

View File

@@ -15,6 +15,7 @@
"required": [],
"type": "object"
},
"instructions": null,
"is_active": true,
"links": [],
"name": "Test Graph",

View File

@@ -15,6 +15,7 @@
"required": [],
"type": "object"
},
"instructions": null,
"is_active": true,
"name": "Test Graph",
"output_schema": {

View File

@@ -11,6 +11,7 @@
"updated_at": "2023-01-01T00:00:00",
"name": "Test Agent 1",
"description": "Test Description 1",
"instructions": null,
"input_schema": {
"type": "object",
"properties": {}
@@ -42,6 +43,7 @@
"updated_at": "2023-01-01T00:00:00",
"name": "Test Agent 2",
"description": "Test Description 2",
"instructions": null,
"input_schema": {
"type": "object",
"properties": {}

View File

@@ -7,6 +7,7 @@
"sub_heading": "Test agent subheading",
"slug": "test-agent",
"description": "Test agent description",
"instructions": null,
"image_urls": [
"test.jpg"
],

View File

@@ -65,6 +65,7 @@ export function AgentRunsView() {
}
/>
</div>
<RunsSidebar
agent={agent}
selectedRunId={selectedRun}

View File

@@ -2,6 +2,8 @@ import { Badge } from "@/components/atoms/Badge/Badge";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Text } from "@/components/atoms/Text/Text";
import { ShowMoreText } from "@/components/molecules/ShowMoreText/ShowMoreText";
import { ClockIcon, InfoIcon } from "@phosphor-icons/react";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
interface ModalHeaderProps {
agent: LibraryAgent;
@@ -9,6 +11,7 @@ interface ModalHeaderProps {
export function ModalHeader({ agent }: ModalHeaderProps) {
const isUnknownCreator = agent.creator_name === "Unknown";
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
@@ -26,6 +29,30 @@ export function ModalHeader({ agent }: ModalHeaderProps) {
>
{agent.description}
</ShowMoreText>
{/* Schedule recommendation tip */}
{agent.recommended_schedule_cron && !agent.has_external_trigger && (
<div className="mt-4 flex items-center gap-2">
<ClockIcon className="h-4 w-4 text-gray-500" />
<p className="text-sm text-gray-600">
<strong>Tip:</strong> For best results, run this agent{" "}
{humanizeCronExpression(
agent.recommended_schedule_cron,
).toLowerCase()}
</p>
</div>
)}
{/* Setup Instructions */}
{agent.instructions && (
<div className="mt-4 flex items-start gap-2">
<InfoIcon className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-500" />
<div className="text-sm text-gray-600">
<strong>Setup Instructions:</strong>{" "}
<span className="whitespace-pre-wrap">{agent.instructions}</span>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -4,6 +4,7 @@ import SchemaTooltip from "@/components/SchemaTooltip";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
import { useRunAgentModalContext } from "../../context";
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
import { InfoIcon } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
import { toDisplayName } from "@/components/integrations/helper";
import { getCredentialTypeDisplayName } from "./helpers";
@@ -64,6 +65,21 @@ export function ModalRunSection() {
</div>
)}
{/* Instructions */}
{agent.instructions && (
<div className="mb-4 flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 p-3">
<InfoIcon className="mt-0.5 h-4 w-4 text-blue-600" />
<div>
<h4 className="text-sm font-medium text-blue-900">
How to use this agent
</h4>
<p className="mt-1 whitespace-pre-wrap text-sm text-blue-800">
{agent.instructions}
</p>
</div>
</div>
)}
{/* Credentials inputs */}
{Object.entries(agentCredentialsInputFields || {}).map(
([key, inputSubSchema]) => (

View File

@@ -16,7 +16,8 @@ import ActionButtonGroup from "@/components/agptui/action-button-group";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { IconCross, IconPlay, IconSave } from "@/components/ui/icons";
import { CalendarClockIcon, Trash2Icon, ClockIcon } from "lucide-react";
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
import { ClockIcon, InfoIcon } from "@phosphor-icons/react";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { ScheduleTaskDialog } from "@/components/cron-scheduler-dialog";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
@@ -587,6 +588,19 @@ export function AgentRunDraftView({
</div>
)}
{/* Setup Instructions */}
{graph.instructions && (
<div className="flex items-start gap-2 rounded-md border border-violet-200 bg-violet-50 p-3">
<InfoIcon className="mt-0.5 h-4 w-4 flex-shrink-0 text-violet-600" />
<div className="text-sm text-violet-800">
<strong>Setup Instructions:</strong>{" "}
<span className="whitespace-pre-wrap">
{graph.instructions}
</span>
</div>
</div>
)}
{(agentPreset || graph.has_external_trigger) && (
<>
{/* Preset name and description */}

View File

@@ -4713,6 +4713,10 @@
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"recommended_schedule_cron": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Recommended Schedule Cron"
@@ -4753,6 +4757,10 @@
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"recommended_schedule_cron": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Recommended Schedule Cron"
@@ -5292,6 +5300,10 @@
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"recommended_schedule_cron": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Recommended Schedule Cron"
@@ -5638,6 +5650,10 @@
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"recommended_schedule_cron": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Recommended Schedule Cron"
@@ -5712,6 +5728,10 @@
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"recommended_schedule_cron": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Recommended Schedule Cron"
@@ -5912,6 +5932,10 @@
},
"name": { "type": "string", "title": "Name" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"input_schema": {
"additionalProperties": true,
"type": "object",
@@ -7335,6 +7359,10 @@
"creator_avatar": { "type": "string", "title": "Creator Avatar" },
"sub_heading": { "type": "string", "title": "Sub Heading" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"categories": {
"items": { "type": "string" },
"type": "array",
@@ -7487,6 +7515,10 @@
"sub_heading": { "type": "string", "title": "Sub Heading" },
"slug": { "type": "string", "title": "Slug" },
"description": { "type": "string", "title": "Description" },
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"image_urls": {
"items": { "type": "string" },
"type": "array",
@@ -7577,6 +7609,10 @@
"title": "Description",
"default": ""
},
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"categories": {
"items": { "type": "string" },
"type": "array",
@@ -7618,6 +7654,10 @@
"title": "Description",
"default": ""
},
"instructions": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Instructions"
},
"categories": {
"items": { "type": "string" },
"type": "array",

View File

@@ -163,6 +163,21 @@ export function AgentInfoStep({
)}
/>
<FormField
control={form.control}
name="instructions"
render={({ field }) => (
<Input
id={field.name}
label="Instructions"
type="textarea"
placeholder="Explain to users how to run this agent and what to expect"
error={form.formState.errors.instructions?.message}
{...field}
/>
)}
/>
<FormField
control={form.control}
name="recommendedScheduleCron"

View File

@@ -41,6 +41,13 @@ export const publishAgentSchema = z.object({
.min(1, "Description is required")
.max(1000, "Description must be less than 1000 characters"),
recommendedScheduleCron: z.string().optional(),
instructions: z
.string()
.optional()
.refine(
(val) => !val || val.length <= 2000,
"Instructions must be less than 2000 characters",
),
});
export type PublishAgentFormData = z.infer<typeof publishAgentSchema>;
@@ -56,4 +63,5 @@ export interface PublishAgentInfoInitialData {
description: string;
additionalImages?: string[];
recommendedScheduleCron?: string;
instructions?: string;
}

View File

@@ -45,6 +45,7 @@ export function useAgentInfoStep({
category: "",
description: "",
recommendedScheduleCron: "",
instructions: "",
},
});
@@ -66,6 +67,7 @@ export function useAgentInfoStep({
category: initialData.category,
description: initialData.description,
recommendedScheduleCron: initialData.recommendedScheduleCron || "",
instructions: initialData.instructions || "",
});
}
}, [initialData, form]);
@@ -94,6 +96,7 @@ export function useAgentInfoStep({
name: data.title,
sub_heading: data.subheader,
description: data.description,
instructions: data.instructions || null,
image_urls: images,
video_url: data.youtubeLink || "",
agent_id: selectedAgentId || "",

View File

@@ -298,6 +298,7 @@ export type GraphMeta = {
is_active: boolean;
name: string;
description: string;
instructions?: string | null;
recommended_schedule_cron: string | null;
forked_from_id?: GraphID | null;
forked_from_version?: number | null;
@@ -428,6 +429,7 @@ export type LibraryAgent = {
updated_at: Date;
name: string;
description: string;
instructions?: string | null;
input_schema: GraphIOSchema;
output_schema: GraphIOSchema;
credentials_input_schema: CredentialsInputSchema;
@@ -746,6 +748,7 @@ export type StoreSubmission = {
name: string;
sub_heading: string;
description: string;
instructions?: string;
image_urls: string[];
date_submitted: string;
status: SubmissionStatus;
@@ -777,6 +780,7 @@ export type StoreSubmissionRequest = {
video_url?: string;
image_urls: string[];
description: string;
instructions?: string | null;
categories: string[];
changes_summary?: string;
recommended_schedule_cron?: string | null;