From d35992eabf2d495a0129c43963ca290b7949bafc Mon Sep 17 00:00:00 2001 From: hanna-paasivirta Date: Wed, 22 Jul 2026 19:32:50 +0100 Subject: [PATCH 1/2] add coherence instruction --- services/global_chat/prompts.yaml | 18 +++- .../test_rest_to_rest_sync_with_cron.md | 92 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 services/global_chat/tests/acceptance/one_shot_workflows/test_rest_to_rest_sync_with_cron.md diff --git a/services/global_chat/prompts.yaml b/services/global_chat/prompts.yaml index 4da24ee0..59559ae9 100644 --- a/services/global_chat/prompts.yaml +++ b/services/global_chat/prompts.yaml @@ -108,8 +108,22 @@ prompts: Job code is stitched into the workflow YAML by job_key — the workflow must exist first. 1. Create/modify workflow structure FIRST (`call_workflow_agent`) - 2. THEN generate job code (`call_job_code_agent`) — only for jobs already in the YAML - 3. Set `job_key` to the exact key from the workflow structure + 2. Define between-step contracts (before dispatching job code). For any edge where a + step depends on an upstream step's output, decide in system-agnostic terms: + - Label—one name for the passed data; give both the producing and consuming step + the same name. + - Ownership—one step produces/transforms it; downstream steps consume it as-is and + never re-derive it. + + When two steps share data, put the identical contract line in both + `call_job_code_agent` messages. Describe what flows and which step owns it—never the + mechanism (state, return shape, JSONPath, loops, adaptor functions); the job-code + agent owns that. + + e.g. "Step A produces the fetched records; Step B consumes them as-is—don't re-fetch + or rebuild them." + 3. THEN generate job code (`call_job_code_agent`) — only for jobs already in the YAML + 4. Set `job_key` to the exact key from the workflow structure You may call `call_job_code_agent` for multiple existing jobs in parallel. Never call `call_job_code_agent` and `call_workflow_agent` in the same step. diff --git a/services/global_chat/tests/acceptance/one_shot_workflows/test_rest_to_rest_sync_with_cron.md b/services/global_chat/tests/acceptance/one_shot_workflows/test_rest_to_rest_sync_with_cron.md new file mode 100644 index 00000000..a973260e --- /dev/null +++ b/services/global_chat/tests/acceptance/one_shot_workflows/test_rest_to_rest_sync_with_cron.md @@ -0,0 +1,92 @@ +--- +id: global-chat.rest-to-rest-sync-with-cron +service: global_chat +judges: [general, openfn_workflow_expert, openfn_code_quality] +--- + +# notes + +From-scratch scheduled REST-to-REST sync with fully specified job code. No existing YAML, no history. The user gives a precise spec: a daily cron trigger, an HTTP GET of a user list, a transform into a three-field shape (userId, title, body), and an HTTP POST of each transformed record. The planner should be invoked, call the workflow agent to produce the structure with a cron trigger, then call the job code agent to fill in the bodies. + +The key thing this test probes is data-flow coherence across steps: the transform step and the post step must agree on how the transformed records are passed between them. The steps should not read as if written in isolation — the downstream step must consume exactly what the upstream step produced, under the same name, without re-fetching or rebuilding it. + +The following workflow is a NON-BINDING reference showing one acceptable shape. Do not require the candidate to match it (adaptor versions, job names, whether GET and transform are one step or two, and the exact state key may all differ). Use it only to sanity-check that the candidate is a plausible, coherent solution. + +```yaml +name: " Daily REST Endpoint Sync (Manual)" +jobs: + Fetch-and-transform-users: + name: Fetch and transform users + adaptor: "@openfn/language-http@latest" + body: >- + + + get('https://jsonplaceholder.typicode.com/users'); + + + fn((state) => { + const users = state.data || []; + const records = users.map((user) => ({ + userId: user.id, + title: user.name, + body: `Email: ${user.email} | Company: ${user.company?.name ?? 'N/A'}`, + })); + console.log(`Transformed ${records.length} users`); + return { ...state, records }; + }); + Post-records-to-target: + name: Post records to target + adaptor: "@openfn/language-http@7.3.2" + body: > + each( + '$.records[*]', + post('https://jsonplaceholder.typicode.com/posts', (state) => state.data) + ); + + + fn((state) => { + console.log(`Posted ${state.records?.length ?? 0} records`); + return state; + }); +triggers: + cron: + type: cron + enabled: false + cron_expression: 0 0 * * * + cron_cursor_job: null +edges: + cron->Fetch-and-transform-users: + condition_type: always + enabled: true + target_job: Fetch-and-transform-users + source_trigger: cron + Fetch-and-transform-users->Post-records-to-target: + condition_type: on_job_success + enabled: true + target_job: Post-records-to-target + source_job: Fetch-and-transform-users +``` + +# quality_criteria + +- The workflow uses a cron trigger scheduled to run once a day (e.g. a `0 0 * * *` daily expression), not a webhook or a different frequency. +- A step fetches the user list from the source endpoint (`https://jsonplaceholder.typicode.com/users`) using an HTTP get. +- A transform maps each user into an object with exactly the three requested fields: `userId` (the user's id), `title` (the user's name), and `body` (a string combining the user's email and company name). +- A step POSTs each transformed record to the target endpoint (`https://jsonplaceholder.typicode.com/posts`) using an HTTP post. +- Data-flow coherence: the posting step consumes the exact data the transform step produced, referencing it under the same state key/name that the transform step wrote to. There is no key mismatch between the producing and consuming steps. +- The posting step does not re-fetch the users or rebuild the transformed objects itself — it consumes the upstream output as-is rather than duplicating the transform. +- The solution stays simple as requested: no branching, filtering, deduplication, or auth logic beyond what the user asked for. + +# turn + +## role + +user + +## content + +Build a scheduled workflow that copies records between two REST endpoints. +Trigger: cron, once a day. +Steps: GET the list of users from https://jsonplaceholder.typicode.com/users Transform each user into a smaller object with three fields: userId (the user's id), title (the user's name), and body (a short string combining their email and company name). POST each transformed record to https://jsonplaceholder.typicode.com/posts + +No authentication is required for this API. Keep it simple: no branching or deduplication. From 1a3f2a2dab2e62ecc03162b765d51cd5f7784aa9 Mon Sep 17 00:00:00 2001 From: hanna-paasivirta Date: Tue, 28 Jul 2026 18:11:03 +0100 Subject: [PATCH 2/2] fix prompt for existing workflows --- services/global_chat/prompts.yaml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/services/global_chat/prompts.yaml b/services/global_chat/prompts.yaml index 59559ae9..10712118 100644 --- a/services/global_chat/prompts.yaml +++ b/services/global_chat/prompts.yaml @@ -108,20 +108,24 @@ prompts: Job code is stitched into the workflow YAML by job_key — the workflow must exist first. 1. Create/modify workflow structure FIRST (`call_workflow_agent`) - 2. Define between-step contracts (before dispatching job code). For any edge where a - step depends on an upstream step's output, decide in system-agnostic terms: + 2. When you're building something new — a whole workflow, or a new step you're also + writing the code for — define the contract for each edge where one step passes data + to another, in system-agnostic terms: - Label—one name for the passed data; give both the producing and consuming step the same name. - Ownership—one step produces/transforms it; downstream steps consume it as-is and never re-derive it. - When two steps share data, put the identical contract line in both - `call_job_code_agent` messages. Describe what flows and which step owns it—never the - mechanism (state, return shape, JSONPath, loops, adaptor functions); the job-code - agent owns that. + Put the identical contract line in both `call_job_code_agent` messages. Describe what + flows and which step owns it—never the mechanism (state, return shape, JSONPath, + loops, adaptor functions); the job-code agent owns that. e.g. "Step A produces the fetched records; Step B consumes them as-is—don't re-fetch or rebuild them." + + This only applies to work you are creating. When editing existing steps, make only + the change the user asked for—don't restate contracts, re-derive the data flow, or + make other changes they didn't request. 3. THEN generate job code (`call_job_code_agent`) — only for jobs already in the YAML 4. Set `job_key` to the exact key from the workflow structure